diff --git a/Modules/Marketplace/Config/config.php b/Modules/Marketplace/Config/config.php new file mode 100644 index 0000000..f18fade --- /dev/null +++ b/Modules/Marketplace/Config/config.php @@ -0,0 +1,13 @@ + (bool) env('MARKETPLACE_ENABLED', true), + 'catalogue_url' => env('MARKETPLACE_CATALOGUE_URL'), + 'allow_remote_install' => (bool) env('MARKETPLACE_ALLOW_REMOTE_INSTALL', false), + 'require_checksum' => (bool) env('MARKETPLACE_REQUIRE_CHECKSUM', true), + 'require_signature' => (bool) env('MARKETPLACE_REQUIRE_SIGNATURE', false), + 'trusted_publishers' => array_values(array_filter(array_map('trim', explode(',', (string) env('MARKETPLACE_TRUSTED_PUBLISHERS', ''))))), + 'storage_disk' => env('MARKETPLACE_STORAGE_DISK', 'local'), + 'package_path' => env('MARKETPLACE_PACKAGE_PATH', 'marketplace/packages'), + 'snapshot_path' => env('MARKETPLACE_SNAPSHOT_PATH', 'marketplace/snapshots'), + 'supported_types' => ['module', 'theme', 'extension'], +]; diff --git a/Modules/Marketplace/Console/Commands/MarketplaceAuditCommand.php b/Modules/Marketplace/Console/Commands/MarketplaceAuditCommand.php new file mode 100644 index 0000000..f8d4a4f --- /dev/null +++ b/Modules/Marketplace/Console/Commands/MarketplaceAuditCommand.php @@ -0,0 +1,14 @@ +with('package')->get(); + foreach ($packages as $package) $this->line(sprintf('%s %s [%s]%s',$package->package_name,$package->version,$package->status,$package->hasUpdate()?' update available':'')); + $this->info($packages->count().' installed package(s) audited.'); return self::SUCCESS; + } +} diff --git a/Modules/Marketplace/Console/Commands/MarketplaceSyncCommand.php b/Modules/Marketplace/Console/Commands/MarketplaceSyncCommand.php new file mode 100644 index 0000000..9625253 --- /dev/null +++ b/Modules/Marketplace/Console/Commands/MarketplaceSyncCommand.php @@ -0,0 +1,12 @@ +warn('MARKETPLACE_CATALOGUE_URL is not configured.'); return self::SUCCESS; } + $this->info('Catalogue synchronisation is configured. Remote installs remain disabled unless explicitly enabled.'); return self::SUCCESS; + } +} diff --git a/Modules/Marketplace/Database/Migrations/2026_07_24_170000_create_marketplace_tables.php b/Modules/Marketplace/Database/Migrations/2026_07_24_170000_create_marketplace_tables.php new file mode 100644 index 0000000..622087d --- /dev/null +++ b/Modules/Marketplace/Database/Migrations/2026_07_24_170000_create_marketplace_tables.php @@ -0,0 +1,48 @@ +id(); $table->string('name'); $table->string('slug')->unique(); $table->string('email')->nullable(); + $table->string('website')->nullable(); $table->text('public_key')->nullable(); $table->boolean('is_verified')->default(false)->index(); + $table->json('metadata')->nullable(); $table->timestamps(); + }); + Schema::create('marketplace_packages', function (Blueprint $table) { + $table->id(); $table->foreignId('publisher_id')->nullable()->constrained('marketplace_publishers')->nullOnDelete(); + $table->string('type',30)->index(); $table->string('name'); $table->string('slug')->unique(); $table->string('package_name')->unique(); + $table->text('description')->nullable(); $table->string('latest_version')->nullable(); $table->string('homepage')->nullable(); + $table->string('repository_url')->nullable(); $table->string('download_url')->nullable(); $table->string('checksum',128)->nullable(); + $table->text('signature')->nullable(); $table->json('manifest')->nullable(); $table->boolean('is_featured')->default(false)->index(); + $table->boolean('is_active')->default(true)->index(); $table->timestamp('catalogue_updated_at')->nullable(); $table->timestamps(); + }); + Schema::create('marketplace_installed_packages', function (Blueprint $table) { + $table->id(); $table->foreignId('marketplace_package_id')->nullable()->constrained('marketplace_packages')->nullOnDelete(); + $table->string('package_name')->unique(); $table->string('type',30)->index(); $table->string('version'); + $table->string('status',30)->default('installed')->index(); $table->json('manifest')->nullable(); $table->string('checksum',128)->nullable(); + $table->string('install_path')->nullable(); $table->boolean('is_enabled')->default(true)->index(); + $table->foreignId('installed_by')->nullable()->constrained('users')->nullOnDelete(); $table->timestamp('installed_at')->nullable(); + $table->timestamp('updated_at_marketplace')->nullable(); $table->timestamps(); + }); + Schema::create('marketplace_installations', function (Blueprint $table) { + $table->id(); $table->foreignId('installed_package_id')->nullable()->constrained('marketplace_installed_packages')->nullOnDelete(); + $table->string('package_name')->index(); $table->string('operation',30)->index(); $table->string('from_version')->nullable(); + $table->string('to_version')->nullable(); $table->string('status',30)->default('pending')->index(); $table->string('snapshot_path')->nullable(); + $table->text('message')->nullable(); $table->json('context')->nullable(); $table->foreignId('performed_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('started_at')->nullable(); $table->timestamp('completed_at')->nullable(); $table->timestamps(); + }); + Schema::create('marketplace_licences', function (Blueprint $table) { + $table->id(); $table->foreignId('marketplace_package_id')->nullable()->constrained('marketplace_packages')->nullOnDelete(); + $table->string('licence_key')->nullable(); $table->string('status',30)->default('inactive')->index(); $table->string('plan')->nullable(); + $table->string('licensed_to')->nullable(); $table->timestamp('activated_at')->nullable(); $table->timestamp('expires_at')->nullable(); + $table->json('metadata')->nullable(); $table->timestamps(); + }); + } + public function down(): void + { + Schema::dropIfExists('marketplace_licences'); Schema::dropIfExists('marketplace_installations'); + Schema::dropIfExists('marketplace_installed_packages'); Schema::dropIfExists('marketplace_packages'); Schema::dropIfExists('marketplace_publishers'); + } +}; diff --git a/Modules/Marketplace/Http/Controllers/MarketplaceApiController.php b/Modules/Marketplace/Http/Controllers/MarketplaceApiController.php new file mode 100644 index 0000000..b4d2499 --- /dev/null +++ b/Modules/Marketplace/Http/Controllers/MarketplaceApiController.php @@ -0,0 +1,16 @@ +json(MarketplacePackage::query()->with('publisher')->where('is_active',true)->when($request->filled('type'),fn($q)=>$q->where('type',$request->string('type')))->paginate(min(100,max(1,$request->integer('per_page',25))))); } + public function installed(): JsonResponse { return response()->json(InstalledPackage::query()->with('package')->paginate(50)); } + public function install(MarketplacePackage $package,MarketplaceManager $manager,Request $request): JsonResponse { return response()->json($manager->install($package,$request->user()?->id)->load('package'),201); } + public function toggle(InstalledPackage $package,MarketplaceManager $manager,Request $request): JsonResponse { return response()->json($manager->setEnabled($package,!$package->is_enabled,$request->user()?->id)); } + public function remove(InstalledPackage $package,MarketplaceManager $manager,Request $request): JsonResponse { $manager->remove($package,$request->user()?->id); return response()->json(['status'=>'removed']); } +} diff --git a/Modules/Marketplace/Http/Controllers/MarketplaceController.php b/Modules/Marketplace/Http/Controllers/MarketplaceController.php new file mode 100644 index 0000000..7eca206 --- /dev/null +++ b/Modules/Marketplace/Http/Controllers/MarketplaceController.php @@ -0,0 +1,24 @@ +with('publisher')->where('is_active',true)->when($request->filled('type'),fn($q)=>$q->where('type',$request->string('type')))->when($request->filled('search'),fn($q)=>$q->where(fn($q)=>$q->where('name','like','%'.$request->string('search').'%')->orWhere('description','like','%'.$request->string('search').'%')))->latest('is_featured')->paginate(20)->withQueryString(); + return view('marketplace::index',compact('packages')); + } + public function installed(): View { $packages=InstalledPackage::query()->with('package')->latest()->paginate(25); return view('marketplace::installed',compact('packages')); } + public function install(MarketplacePackage $package,MarketplaceManager $manager,Request $request): RedirectResponse { $manager->install($package,$request->user()?->id); return back()->with('status','Package installed successfully.'); } + public function update(MarketplacePackage $package,MarketplaceManager $manager,Request $request): RedirectResponse { $manager->install($package,$request->user()?->id); return back()->with('status','Package updated successfully.'); } + public function toggle(InstalledPackage $package,MarketplaceManager $manager,Request $request): RedirectResponse { $manager->setEnabled($package,!$package->is_enabled,$request->user()?->id); return back()->with('status','Package state updated.'); } + public function remove(InstalledPackage $package,MarketplaceManager $manager,Request $request): RedirectResponse { $manager->remove($package,$request->user()?->id); return back()->with('status','Package removed.'); } + public function rollback(InstalledPackage $package,Installation $installation,MarketplaceManager $manager,Request $request): RedirectResponse { $manager->rollback($package,$installation,$request->user()?->id); return back()->with('status','Package rolled back.'); } +} diff --git a/Modules/Marketplace/Models/Installation.php b/Modules/Marketplace/Models/Installation.php new file mode 100644 index 0000000..6573279 --- /dev/null +++ b/Modules/Marketplace/Models/Installation.php @@ -0,0 +1,9 @@ +'array','started_at'=>'datetime','completed_at'=>'datetime']; +} diff --git a/Modules/Marketplace/Models/InstalledPackage.php b/Modules/Marketplace/Models/InstalledPackage.php new file mode 100644 index 0000000..c2dace8 --- /dev/null +++ b/Modules/Marketplace/Models/InstalledPackage.php @@ -0,0 +1,14 @@ +'array','is_enabled'=>'boolean','installed_at'=>'datetime','updated_at_marketplace'=>'datetime']; + public function package(): BelongsTo { return $this->belongsTo(MarketplacePackage::class,'marketplace_package_id'); } + public function installations(): HasMany { return $this->hasMany(Installation::class,'installed_package_id'); } + public function hasUpdate(): bool { return $this->package?->latest_version && version_compare($this->package->latest_version,$this->version,'>'); } +} diff --git a/Modules/Marketplace/Models/MarketplaceLicence.php b/Modules/Marketplace/Models/MarketplaceLicence.php new file mode 100644 index 0000000..48ef02a --- /dev/null +++ b/Modules/Marketplace/Models/MarketplaceLicence.php @@ -0,0 +1,10 @@ +'datetime','expires_at'=>'datetime','metadata'=>'array']; +} diff --git a/Modules/Marketplace/Models/MarketplacePackage.php b/Modules/Marketplace/Models/MarketplacePackage.php new file mode 100644 index 0000000..7134aa6 --- /dev/null +++ b/Modules/Marketplace/Models/MarketplacePackage.php @@ -0,0 +1,13 @@ +'array','is_featured'=>'boolean','is_active'=>'boolean','catalogue_updated_at'=>'datetime']; + public function publisher(): BelongsTo { return $this->belongsTo(Publisher::class); } + public function licences(): HasMany { return $this->hasMany(MarketplaceLicence::class,'marketplace_package_id'); } +} diff --git a/Modules/Marketplace/Models/Publisher.php b/Modules/Marketplace/Models/Publisher.php new file mode 100644 index 0000000..b74acba --- /dev/null +++ b/Modules/Marketplace/Models/Publisher.php @@ -0,0 +1,11 @@ +'boolean','metadata'=>'array']; + public function packages(): HasMany { return $this->hasMany(MarketplacePackage::class,'publisher_id'); } +} diff --git a/Modules/Marketplace/Providers/MarketplaceServiceProvider.php b/Modules/Marketplace/Providers/MarketplaceServiceProvider.php new file mode 100644 index 0000000..9902c22 --- /dev/null +++ b/Modules/Marketplace/Providers/MarketplaceServiceProvider.php @@ -0,0 +1,30 @@ +mergeConfigFrom(module_path('Marketplace', 'Config/config.php'), 'marketplace'); + $this->app->singleton(PackageManifest::class); + $this->app->singleton(CompatibilityChecker::class); + $this->app->singleton(DependencyResolver::class); + $this->app->singleton(PackageVerifier::class); + $this->app->singleton(MarketplaceManager::class); + $this->app->register(RouteServiceProvider::class); + } + public function boot(): void + { + $this->loadMigrationsFrom(module_path('Marketplace', 'Database/Migrations')); + $this->loadViewsFrom(module_path('Marketplace', 'Resources/views'), 'marketplace'); + $this->loadTranslationsFrom(module_path('Marketplace', 'Resources/lang'), 'marketplace'); + if ($this->app->runningInConsole()) { + $this->commands([\Modules\Marketplace\Console\Commands\MarketplaceSyncCommand::class, \Modules\Marketplace\Console\Commands\MarketplaceAuditCommand::class]); + } + } +} diff --git a/Modules/Marketplace/Providers/RouteServiceProvider.php b/Modules/Marketplace/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..1f248db --- /dev/null +++ b/Modules/Marketplace/Providers/RouteServiceProvider.php @@ -0,0 +1,12 @@ +group(module_path('Marketplace', 'Routes/web.php')); + Route::middleware('api')->prefix('api/v1')->group(module_path('Marketplace', 'Routes/api.php')); + } +} diff --git a/Modules/Marketplace/Resources/views/index.blade.php b/Modules/Marketplace/Resources/views/index.blade.php new file mode 100644 index 0000000..6399b57 --- /dev/null +++ b/Modules/Marketplace/Resources/views/index.blade.php @@ -0,0 +1,97 @@ + +
+
+
+ Marketplace + + Discover verified modules, themes and extensions. + +
+ + + Installed packages + +
+ +
+ + + + + + @foreach (config('marketplace.supported_types', []) as $type) + + @endforeach + + + Filter + + +
+ @forelse ($packages as $package) +
+
+
+
+

{{ $package->name }}

+ + @if ($package->is_featured) + Featured + @endif +
+ +

+ {{ $package->package_name }} + · {{ \Illuminate\Support\Str::headline($package->type) }} + · {{ $package->latest_version ?: 'No release' }} +

+
+ + @can('marketplace.install') +
+ @csrf + + + Install + +
+ @endcan +
+ +

{{ $package->description }}

+ + @if ($package->publisher) +

+ Published by {{ $package->publisher->name }} + + @if ($package->publisher->is_verified) + · Verified + @endif +

+ @endif +
+ @empty +
+ No marketplace packages found. +
+ @endforelse +
+ + {{ $packages->links() }} +
+
diff --git a/Modules/Marketplace/Resources/views/installed.blade.php b/Modules/Marketplace/Resources/views/installed.blade.php new file mode 100644 index 0000000..7609352 --- /dev/null +++ b/Modules/Marketplace/Resources/views/installed.blade.php @@ -0,0 +1,104 @@ + +
+
+
+ Installed Packages + + Manage package state, updates and removals. + +
+ + + Browse marketplace + +
+ +
+ + + + + + + + + + + + + @forelse ($packages as $package) + + + + + + + + + + @empty + + + + @endforelse + +
PackageVersionStatusUpdateActions
+
{{ $package->package_name }}
+
+ {{ \Illuminate\Support\Str::headline($package->type) }} +
+
{{ $package->version }}{{ ucfirst($package->status) }} + {{ $package->hasUpdate() ? $package->package->latest_version : 'Current' }} + +
+ @can('marketplace.update') +
+ @csrf + + + {{ $package->is_enabled ? 'Disable' : 'Enable' }} + +
+ + @if ($package->hasUpdate()) +
+ @csrf + + + Update + +
+ @endif + @endcan + + @can('marketplace.remove') +
+ @csrf + @method('DELETE') + + + Remove + +
+ @endcan +
+
+ No packages installed through the marketplace. +
+
+ + {{ $packages->links() }} +
+
diff --git a/Modules/Marketplace/Routes/api.php b/Modules/Marketplace/Routes/api.php new file mode 100644 index 0000000..ca55eef --- /dev/null +++ b/Modules/Marketplace/Routes/api.php @@ -0,0 +1,10 @@ +prefix('marketplace')->name('api.marketplace.')->group(function () { + Route::get('/packages',[MarketplaceApiController::class,'packages'])->middleware('permission:marketplace.view')->name('packages'); + Route::get('/installed',[MarketplaceApiController::class,'installed'])->middleware('permission:marketplace.view')->name('installed'); + Route::post('/packages/{package}/install',[MarketplaceApiController::class,'install'])->middleware('permission:marketplace.install')->name('install'); + Route::post('/installed/{package}/toggle',[MarketplaceApiController::class,'toggle'])->middleware('permission:marketplace.update')->name('toggle'); + Route::delete('/installed/{package}',[MarketplaceApiController::class,'remove'])->middleware('permission:marketplace.remove')->name('remove'); +}); diff --git a/Modules/Marketplace/Routes/web.php b/Modules/Marketplace/Routes/web.php new file mode 100644 index 0000000..e92e32f --- /dev/null +++ b/Modules/Marketplace/Routes/web.php @@ -0,0 +1,12 @@ +prefix('administration/marketplace')->name('administration.marketplace.')->group(function () { + Route::get('/',[MarketplaceController::class,'index'])->middleware('permission:marketplace.view')->name('index'); + Route::get('/installed',[MarketplaceController::class,'installed'])->middleware('permission:marketplace.view')->name('installed'); + Route::post('/packages/{package}/install',[MarketplaceController::class,'install'])->middleware('permission:marketplace.install')->name('install'); + Route::post('/packages/{package}/update',[MarketplaceController::class,'update'])->middleware('permission:marketplace.update')->name('update'); + Route::post('/installed/{package}/toggle',[MarketplaceController::class,'toggle'])->middleware('permission:marketplace.update')->name('toggle'); + Route::delete('/installed/{package}',[MarketplaceController::class,'remove'])->middleware('permission:marketplace.remove')->name('remove'); + Route::post('/installed/{package}/rollback/{installation}',[MarketplaceController::class,'rollback'])->middleware('permission:marketplace.update')->name('rollback'); +}); diff --git a/Modules/Marketplace/Services/CompatibilityChecker.php b/Modules/Marketplace/Services/CompatibilityChecker.php new file mode 100644 index 0000000..5fd7f96 --- /dev/null +++ b/Modules/Marketplace/Services/CompatibilityChecker.php @@ -0,0 +1,17 @@ +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]; + } +} diff --git a/Modules/Marketplace/Services/DependencyResolver.php b/Modules/Marketplace/Services/DependencyResolver.php new file mode 100644 index 0000000..dc97614 --- /dev/null +++ b/Modules/Marketplace/Services/DependencyResolver.php @@ -0,0 +1,17 @@ +$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]; + } +} diff --git a/Modules/Marketplace/Services/MarketplaceManager.php b/Modules/Marketplace/Services/MarketplaceManager.php new file mode 100644 index 0000000..c3d8741 --- /dev/null +++ b/Modules/Marketplace/Services/MarketplaceManager.php @@ -0,0 +1,39 @@ +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 + { + $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 + { + 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 + { + 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()]); } +} diff --git a/Modules/Marketplace/Services/PackageManifest.php b/Modules/Marketplace/Services/PackageManifest.php new file mode 100644 index 0000000..6a7de47 --- /dev/null +++ b/Modules/Marketplace/Services/PackageManifest.php @@ -0,0 +1,22 @@ +'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'] ?? '*'; + return $manifest; + } + public function fromFile(string $path): array { return $this->parse((string) file_get_contents($path)); } +} diff --git a/Modules/Marketplace/Services/PackageVerifier.php b/Modules/Marketplace/Services/PackageVerifier.php new file mode 100644 index 0000000..647e00c --- /dev/null +++ b/Modules/Marketplace/Services/PackageVerifier.php @@ -0,0 +1,16 @@ +true,'checksum'=>$actual,'signature_present'=>filled($manifest['signature'] ?? null)]; + } +} diff --git a/Modules/Marketplace/Services/VersionConstraint.php b/Modules/Marketplace/Services/VersionConstraint.php new file mode 100644 index 0000000..d0919c0 --- /dev/null +++ b/Modules/Marketplace/Services/VersionConstraint.php @@ -0,0 +1,22 @@ +=') && 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; + } +} diff --git a/Modules/Marketplace/Tests/Feature/MarketplaceModuleTest.php b/Modules/Marketplace/Tests/Feature/MarketplaceModuleTest.php new file mode 100644 index 0000000..e02803a --- /dev/null +++ b/Modules/Marketplace/Tests/Feature/MarketplaceModuleTest.php @@ -0,0 +1,18 @@ +parse(['name'=>'vendor/example','type'=>'module','version'=>'1.0.0']); + $this->assertSame('module',$manifest['type']); $this->assertSame([], $manifest['requires']); + } + public function test_current_platform_is_compatible_with_wildcard_manifest(): void + { + $result=app(CompatibilityChecker::class)->check(['php'=>'*','laravel'=>'*','content_manager'=>'*']); + $this->assertTrue($result['compatible']); + } +} diff --git a/Modules/Marketplace/module.json b/Modules/Marketplace/module.json new file mode 100644 index 0000000..936c435 --- /dev/null +++ b/Modules/Marketplace/module.json @@ -0,0 +1 @@ +{"name":"Marketplace","alias":"marketplace","version":"1.0.0","description":"Secure package catalogue, installation, updates, licences, publishers and rollback management.","keywords":["marketplace","modules","themes","extensions","packages"],"priority":170,"providers":["Modules\\Marketplace\\Providers\\MarketplaceServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit","Developer","Security"]} diff --git a/Modules/Roles/Database/Seeders/RolePermissionSeeder.php b/Modules/Roles/Database/Seeders/RolePermissionSeeder.php index ea8bc99..ea2284d 100644 --- a/Modules/Roles/Database/Seeders/RolePermissionSeeder.php +++ b/Modules/Roles/Database/Seeders/RolePermissionSeeder.php @@ -80,6 +80,7 @@ final class RolePermissionSeeder extends Seeder 'analytics.manage', 'analytics.export', 'builder.view', 'builder.manage', 'builder.publish', + 'marketplace.view', 'marketplace.install', 'marketplace.update', 'marketplace.remove', ]; foreach ($permissions as $permission) { diff --git a/modules_statuses.json b/modules_statuses.json index 8b16202..83eb280 100644 --- a/modules_statuses.json +++ b/modules_statuses.json @@ -42,5 +42,6 @@ "Automation": true, "AI": true, "Analytics": true, - "Builder": true + "Builder": true, + "Marketplace": true } diff --git a/phpunit.xml b/phpunit.xml index e86b194..9e92c55 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,18 +5,26 @@ colors="true" > - - tests/Unit + + tests/Unit - - tests/Feature + + + tests/Feature + + + + Modules + - app + app + Modules + diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index 27f3f87..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); \ No newline at end of file