Check dependencies and conflicts before installing plugin

fixes #109
This commit is contained in:
Pig Fang 2019-12-09 23:30:49 +08:00
parent eb62e04c1c
commit f9b32c8e69
13 changed files with 111 additions and 43 deletions

View File

@ -82,6 +82,15 @@ class MarketController extends Controller
return json(trans('admin.plugins.market.non-existent', ['plugin' => $name]), 1);
}
$fakePlugin = new Plugin('', $metadata);
$unsatisfied = $manager->getUnsatisfied($fakePlugin);
$conflicts = $manager->getConflicts($fakePlugin);
if ($unsatisfied->isNotEmpty() || $conflicts->isNotEmpty()) {
$reason = $manager->formatUnresolved($unsatisfied, $conflicts);
return json(trans('admin.plugins.market.unresolved'), 1, compact('reason'));
}
$url = $metadata['dist']['url'];
$filename = Arr::last(explode('/', $url));
$pluginsDir = $manager->getPluginsDirs()->first();

View File

@ -59,27 +59,7 @@ class PluginController extends Controller
if ($result === true) {
return json(trans('admin.plugins.operations.enabled', ['plugin' => $plugin->title]), 0);
} else {
$unsatisfied = $result['unsatisfied']->map(function ($detail, $name) use ($plugins) {
$constraint = $detail['constraint'];
if (! $detail['version']) {
$plugin = $plugins->get($name);
$name = $plugin ? trans($plugin->title) : $name;
return trans('admin.plugins.operations.unsatisfied.disabled', compact('name'));
} else {
$title = trans($plugins->get($name)->title);
return trans('admin.plugins.operations.unsatisfied.version', compact('title', 'constraint'));
}
})->values()->all();
$conflicts = $result['conflicts']->map(function ($detail, $name) use ($plugins) {
$title = trans($plugins->get($name)->title);
return trans('admin.plugins.operations.unsatisfied.conflict', compact('title'));
})->values()->all();
$reason = array_merge($unsatisfied, $conflicts);
$reason = $plugins->formatUnresolved($result['unsatisfied'], $result['conflicts']);
return json(trans('admin.plugins.operations.unsatisfied.notice'), 1, compact('reason'));
}

View File

@ -405,6 +405,36 @@ class PluginManager
});
}
/**
* Format the "unresolved" information into human-readable text.
*/
public function formatUnresolved(
Collection $unsatisfied,
Collection $conflicts
): array {
$unsatisfied = $unsatisfied->map(function ($detail, $name) {
$constraint = $detail['constraint'];
if (! $detail['version']) {
$plugin = $this->get($name);
$name = $plugin ? trans($plugin->title) : $name;
return trans('admin.plugins.operations.unsatisfied.disabled', compact('name'));
} else {
$title = trans($this->get($name)->title);
return trans('admin.plugins.operations.unsatisfied.version', compact('title', 'constraint'));
}
})->values()->all();
$conflicts = $conflicts->map(function ($detail, $name) {
$title = trans($this->get($name)->title);
return trans('admin.plugins.operations.unsatisfied.conflict', compact('title'));
})->values()->all();
return array_merge($unsatisfied, $conflicts);
}
/**
* The plugins path.
*

View File

@ -0,0 +1,19 @@
import { showModal } from '../../scripts/notify'
export default function (message: string, reason: string[]): void {
const div = document.createElement('div')
const p = document.createElement('p')
p.textContent = message
div.appendChild(p)
const ul = document.createElement('ul')
reason.forEach(item => {
const li = document.createElement('li')
li.textContent = item
ul.appendChild(li)
})
div.appendChild(ul)
showModal({
mode: 'alert',
dangerousHTML: div.outerHTML,
})
}

View File

@ -1,5 +1,6 @@
import Vue from 'vue'
import { showModal, toast } from '../../scripts/notify'
import alertUnresolvedPlugins from './alertUnresolvedPlugins'
export default Vue.extend({
data: () => ({ plugins: [] }),
@ -32,21 +33,7 @@ export default Vue.extend({
toast.success(message)
this.$set(this.plugins[originalIndex], 'enabled', true)
} else {
const div = document.createElement('div')
const p = document.createElement('p')
p.textContent = message
div.appendChild(p)
const ul = document.createElement('ul')
reason.forEach(item => {
const li = document.createElement('li')
li.textContent = item
ul.appendChild(li)
})
div.appendChild(ul)
showModal({
mode: 'alert',
dangerousHTML: div.outerHTML,
})
alertUnresolvedPlugins(message, reason)
}
},
},

View File

@ -73,6 +73,7 @@
<script>
import { VueGoodTable } from 'vue-good-table'
import 'vue-good-table/dist/vue-good-table.min.css'
import alertUnresolvedPlugins from '../../components/mixins/alertUnresolvedPlugins'
import enablePlugin from '../../components/mixins/enablePlugin'
import tableOptions from '../../components/mixins/tableOptions'
import emitMounted from '../../components/mixins/emitMounted'
@ -134,7 +135,11 @@ export default {
async installPlugin({ name, originalIndex }) {
this.installing = name
const { code, message } = await this.$http.post(
const {
code,
message,
data,
} = await this.$http.post(
'/admin/plugins/market/download',
{ name },
)
@ -142,6 +147,8 @@ export default {
toast.success(message)
this.plugins[originalIndex].update_available = false
this.plugins[originalIndex].installed = true
} else if (data && data.reason) {
alertUnresolvedPlugins(message, data.reason)
} else {
toast.error(message)
}

View File

@ -57,6 +57,11 @@ test('install plugin', async () => {
])
Vue.prototype.$http.post
.mockResolvedValueOnce({ code: 1, message: '1' })
.mockResolvedValueOnce({
code: 1,
message: 'unresolved',
data: { reason: ['u'] },
})
.mockResolvedValueOnce({ code: 0, message: '0' })
const wrapper = mount(Market)
await flushPromises()
@ -68,6 +73,11 @@ test('install plugin', async () => {
'/admin/plugins/market/download',
{ name: 'd' },
)
button.trigger('click')
await flushPromises()
expect(showModal).toBeCalledWith(expect.objectContaining({ mode: 'alert' }))
button.trigger('click')
await flushPromises()
expect(wrapper.text()).toContain('admin.enablePlugin')

View File

@ -134,6 +134,7 @@ plugins:
not-found: No such plugin.
market:
unresolved: There are conflicts or unsatisfied dependencies in the plugin, therefore we can't download it. Please install or update the plugins listed below, and disable those have conflicts.
connection-error: Unable to connect to the plugins registry. :error
non-existent: The plugin :plugin does not exist.
install-success: Plugin was installed.

View File

@ -125,6 +125,7 @@ plugins:
no-readme-notice: The plugin doesn't contain a readme file.
not-found: No existe dicho plugin.
market:
unresolved: Hay conflictos o dependencias insatisfechas en el plugin, por lo tanto no podemos descargar. Por favor, instale o actualice los plugins listados a continuación, y desactive los que tienen conflictos.
connection-error: No se puede conectar al registro de plugins. :error
non-existent: El plugin :plugin no existe.
install-success: Se instaló el plugin.

View File

@ -125,6 +125,7 @@ plugins:
no-readme-notice: 这个插件没有说明文件。
not-found: 插件不存在
market:
unresolved: 无法下载此插件,因为其仍有冲突或未满足的依赖关系。请检查以下插件的版本,更新或安装它们并禁用存在冲突的插件:
connection-error: 无法连接至插件市场源,错误信息::error
non-existent: 插件 :plugin 不存在
install-success: 插件安装成功

View File

@ -32,6 +32,7 @@
- Fixed fallback when unknown locale is detected.
- Fixed compatibility with PHP 7.4.
- Fixed the display problem for too long texture name.
- Fixed that dependencies and conflicts haven't been checked before installing plugin.
## Removed

View File

@ -32,6 +32,7 @@
- 修复不能针对未知语言进行降级的问题
- 与 PHP 7.4 的兼容问题
- 材质名过长时的显示问题
- 下载插件前不检查依赖和冲突的问题
## 移除

View File

@ -36,6 +36,26 @@ class MarketControllerTest extends TestCase
'message' => trans('admin.plugins.market.non-existent', ['plugin' => 'non-existent-plugin']),
]);
// Unresolved plugin.
$fakeRegistry = json_encode(['packages' => [
[
'name' => 'fake',
'version' => '0.0.0',
'require' => ['a' => '^4.0.0'],
],
]]);
$this->appendToGuzzleQueue([new Response(200, [], $fakeRegistry)]);
$this->postJson('/admin/plugins/market/download', ['name' => 'fake'])
->assertJson([
'message' => trans('admin.plugins.market.unresolved'),
'code' => 1,
'data' => [
'reason' => [
trans('admin.plugins.operations.unsatisfied.disabled', ['name' => 'a']),
],
],
]);
// Download
$fakeRegistry = json_encode(['packages' => [
[
@ -51,9 +71,8 @@ class MarketControllerTest extends TestCase
->once()
->andThrow(new \Exception());
});
$this->postJson('/admin/plugins/market/download', [
'name' => 'fake',
])->assertJson(['code' => 1]);
$this->postJson('/admin/plugins/market/download', ['name' => 'fake'])
->assertJson(['code' => 1]);
$this->appendToGuzzleQueue([new Response(200, [], $fakeRegistry)]);
$this->mock(PackageManager::class, function ($mock) {
@ -65,9 +84,11 @@ class MarketControllerTest extends TestCase
->with(base_path('plugins'))
->once();
});
$this->postJson('/admin/plugins/market/download', [
'name' => 'fake',
])->assertJson(['code' => 0, 'message' => trans('admin.plugins.market.install-success')]);
$this->postJson('/admin/plugins/market/download', ['name' => 'fake'])
->assertJson([
'code' => 0,
'message' => trans('admin.plugins.market.install-success')
]);
}
public function testCheckUpdates()