refactor plugins marketplace

This commit is contained in:
Pig Fang 2020-01-14 11:37:21 +08:00
parent 078fcf47ec
commit 4c735aa8e4
4 changed files with 43 additions and 81 deletions

View File

@ -8,46 +8,28 @@ use App\Services\Unzip;
use Composer\CaBundle\CaBundle; use Composer\CaBundle\CaBundle;
use Composer\Semver\Comparator; use Composer\Semver\Comparator;
use Exception; use Exception;
use GuzzleHttp\Client;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class MarketController extends Controller class MarketController extends Controller
{ {
/** public function marketData(PluginManager $manager, Client $client)
* Guzzle HTTP client.
*
* @var \GuzzleHttp\Client
*/
protected $guzzle;
/**
* Cache for plugins registry.
*
* @var array
*/
protected $registryCache;
public function __construct(\GuzzleHttp\Client $guzzle)
{ {
$this->guzzle = $guzzle; $plugins = $this->fetch($client)->map(function ($item) use ($manager) {
}
public function marketData(PluginManager $manager)
{
$plugins = collect($this->getAllAvailablePlugins())->map(function ($item) use ($manager) {
$plugin = $manager->get($item['name']); $plugin = $manager->get($item['name']);
if ($plugin) { if ($plugin) {
$item['enabled'] = $plugin->isEnabled(); $item['enabled'] = $plugin->isEnabled();
$item['installed'] = $plugin->version; $item['installed'] = $plugin->version;
$item['update_available'] = Comparator::greaterThan($item['version'], $item['installed']); $item['can_update'] = Comparator::greaterThan($item['version'], $item['installed']);
} else { } else {
$item['installed'] = false; $item['installed'] = false;
} }
$requirements = Arr::get($item, 'require', []); $requirements = Arr::get($item, 'require', []);
unset($item['require']); unset($item['require']);
$item['dependencies'] = [ $item['dependencies'] = [
'all' => $requirements, 'all' => $requirements,
'unsatisfied' => $manager->getUnsatisfied(new Plugin('', $item)), 'unsatisfied' => $manager->getUnsatisfied(new Plugin('', $item)),
@ -59,10 +41,15 @@ class MarketController extends Controller
return $plugins; return $plugins;
} }
public function download(Request $request, PluginManager $manager, Unzip $unzip) public function download(
{ Request $request,
$name = $request->get('name'); PluginManager $manager,
$metadata = $this->getPluginMetadata($name); Client $client,
Unzip $unzip
) {
$name = $request->input('name');
$plugins = $this->fetch($client);
$metadata = $plugins->firstWhere('name', $name);
if (!$metadata) { if (!$metadata) {
return json(trans('admin.plugins.market.non-existent', ['plugin' => $name]), 1); return json(trans('admin.plugins.market.non-existent', ['plugin' => $name]), 1);
@ -77,56 +64,38 @@ class MarketController extends Controller
return json(trans('admin.plugins.market.unresolved'), 1, compact('reason')); return json(trans('admin.plugins.market.unresolved'), 1, compact('reason'));
} }
$url = $metadata['dist']['url']; $path = tempnam(sys_get_temp_dir(), $name);
$pluginsDir = $manager->getPluginsDirs()->first();
$path = storage_path("packages/$name".'_'.$metadata['version'].'.zip');
try { try {
$this->guzzle->get($url, [ $client->get($metadata['dist']['url'], [
'sink' => $path, 'sink' => $path,
'verify' => CaBundle::getSystemCaRootBundlePath(), 'verify' => CaBundle::getSystemCaRootBundlePath(),
]); ]);
$unzip->extract($path, $pluginsDir); $unzip->extract($path, $manager->getPluginsDirs()->first());
} catch (Exception $e) { } catch (Exception $e) {
return json($e->getMessage(), 1); report($e);
return json(trans('admin.download.errors.download', ['error' => $e->getMessage()]), 1);
} }
return json(trans('admin.plugins.market.install-success'), 0); return json(trans('admin.plugins.market.install-success'), 0);
} }
protected function getPluginMetadata($name) protected function fetch(Client $client): Collection
{ {
return collect($this->getAllAvailablePlugins())->firstWhere('name', $name); $plugins = collect(explode(',', config('plugins.registry')))
} ->map(function ($registry) use ($client) {
protected function getAllAvailablePlugins()
{
$registryVersion = 1;
if (app()->runningUnitTests() || !$this->registryCache) {
$registries = collect(explode(',', config('plugins.registry')));
$this->registryCache = $registries->map(function ($registry) use ($registryVersion) {
try { try {
$pluginsJson = $this->guzzle->request( $body = $client->get(trim($registry), [
'GET', 'verify' => CaBundle::getSystemCaRootBundlePath(),
trim($registry), ])->getBody();
['verify' => \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath()]
)->getBody();
} catch (Exception $e) { } catch (Exception $e) {
throw new Exception(trans('admin.plugins.market.connection-error', ['error' => htmlentities($e->getMessage())])); throw new Exception(trans('admin.plugins.market.connection-error', ['error' => $e->getMessage()]));
} }
$registryData = json_decode($pluginsJson, true); return Arr::get(json_decode($body, true), 'packages', []);
$received = Arr::get($registryData, 'version'); })
abort_if( ->flatten(1);
is_int($received) && $received != $registryVersion,
500,
"Only version $registryVersion of market registry is accepted."
);
return Arr::get($registryData, 'packages', []); return $plugins;
})->flatten(1);
}
return $this->registryCache;
} }
} }

View File

@ -31,7 +31,7 @@
<span v-else-if="props.column.field === 'operations'"> <span v-else-if="props.column.field === 'operations'">
<template v-if="props.row.installed"> <template v-if="props.row.installed">
<button <button
v-if="props.row.update_available" v-if="props.row.can_update"
class="btn btn-success" class="btn btn-success"
:disabled="installing === props.row.name" :disabled="installing === props.row.name"
@click="updatePlugin(props.row)" @click="updatePlugin(props.row)"
@ -145,7 +145,7 @@ export default {
) )
if (code === 0) { if (code === 0) {
toast.success(message) toast.success(message)
this.plugins[originalIndex].update_available = false this.plugins[originalIndex].can_update = false
this.plugins[originalIndex].installed = true this.plugins[originalIndex].installed = true
} else if (data && data.reason) { } else if (data && data.reason) {
alertUnresolvedPlugins(message, data.reason) alertUnresolvedPlugins(message, data.reason)

View File

@ -27,7 +27,7 @@ test('render dependencies', async () => {
test('render operation buttons', async () => { test('render operation buttons', async () => {
Vue.prototype.$http.get.mockResolvedValue([ Vue.prototype.$http.get.mockResolvedValue([
{ {
name: 'a', dependencies: { all: {}, unsatisfied: {} }, installed: true, update_available: true, name: 'a', dependencies: { all: {}, unsatisfied: {} }, installed: true, can_update: true,
}, },
{ {
name: 'b', dependencies: { all: {}, unsatisfied: {} }, installed: true, enabled: true, name: 'b', dependencies: { all: {}, unsatisfied: {} }, installed: true, enabled: true,
@ -90,7 +90,7 @@ test('update plugin', async () => {
version: '2.0.0', version: '2.0.0',
dependencies: { all: {}, unsatisfied: {} }, dependencies: { all: {}, unsatisfied: {} },
installed: '1.0.0', installed: '1.0.0',
update_available: true, can_update: true,
}, },
]) ])
Vue.prototype.$http.post Vue.prototype.$http.post

View File

@ -17,24 +17,22 @@ class MarketControllerTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->actAs('superAdmin'); $this->actingAs(factory(\App\Models\User::class, 'superAdmin')->create());
} }
public function testDownload() public function testDownload()
{ {
$this->setupGuzzleClientMock(); $this->setupGuzzleClientMock();
// Try to download a non-existent plugin
$this->appendToGuzzleQueue(200, [], json_encode([ $this->appendToGuzzleQueue(200, [], json_encode([
'version' => 1, 'version' => 1,
'packages' => [], 'packages' => [],
])); ]));
$this->postJson('/admin/plugins/market/download', [ $this->postJson('/admin/plugins/market/download', ['name' => 'nope'])
'name' => 'non-existent-plugin', ->assertJson([
])->assertJson([ 'code' => 1,
'code' => 1, 'message' => trans('admin.plugins.market.non-existent', ['plugin' => 'nope']),
'message' => trans('admin.plugins.market.non-existent', ['plugin' => 'non-existent-plugin']), ]);
]);
// Unresolved plugin. // Unresolved plugin.
$fakeRegistry = json_encode(['packages' => [ $fakeRegistry = json_encode(['packages' => [
@ -88,7 +86,7 @@ class MarketControllerTest extends TestCase
public function testMarketData() public function testMarketData()
{ {
$this->setupGuzzleClientMock([ $this->setupGuzzleClientMock([
new RequestException('Connection Error', new Request('POST', 'whatever')), new RequestException('Connection Error', new Request('POST', '')),
new Response(200, [], json_encode(['version' => 1, 'packages' => [ new Response(200, [], json_encode(['version' => 1, 'packages' => [
[ [
'name' => 'fake1', 'name' => 'fake1',
@ -109,11 +107,9 @@ class MarketControllerTest extends TestCase
'require' => [], 'require' => [],
], ],
]])), ]])),
new Response(200, [], json_encode(['version' => 0])),
]); ]);
// Expected an exception, but unable to be asserted. $this->getJson('/admin/plugins/market/list')->assertStatus(500);
$this->getJson('/admin/plugins/market/list');
$this->mock(PluginManager::class, function ($mock) { $this->mock(PluginManager::class, function ($mock) {
$mock->shouldReceive('get') $mock->shouldReceive('get')
@ -139,8 +135,5 @@ class MarketControllerTest extends TestCase
'dependencies', 'dependencies',
], ],
]); ]);
$this->getJson('/admin/plugins/market/list')
->assertJson(['message' => 'Only version 1 of market registry is accepted.']);
} }
} }