diff --git a/app/Http/Controllers/MarketController.php b/app/Http/Controllers/MarketController.php
new file mode 100644
index 00000000..35d7964c
--- /dev/null
+++ b/app/Http/Controllers/MarketController.php
@@ -0,0 +1,127 @@
+getAllAvailablePlugins())->map(function ($item) {
+ $plugin = plugin($item['name']);
+ $manager = app('plugins');
+
+ if ($plugin) {
+ $item['enabled'] = $plugin->isEnabled();
+ $item['installed'] = $plugin->version;
+ $item['update_available'] = Comparator::greaterThan($item['version'], $item['installed']);
+ } else {
+ $item['installed'] = false;
+ }
+
+ $requirements = array_get($item, 'require', []);
+ unset($item['require']);
+
+ $item['dependencies'] = [
+ 'isRequirementsSatisfied' => $manager->isRequirementsSatisfied($requirements),
+ 'requirements' => $requirements,
+ 'unsatisfiedRequirements' => $manager->getUnsatisfiedRequirements($requirements)
+ ];
+
+ return $item;
+ });
+
+ return Datatables::of($plugins)->setRowId('plugin-{{ $name }}')->make(true);
+ }
+
+ public function download(Request $request, PluginManager $manager)
+ {
+ $name = $request->get('name');
+ $metadata = $this->getPluginMetadata($name);
+
+ if (! $metadata) {
+ return json(trans('admin.plugins.market.non-existent', ['plugin' => $name]), 1);
+ }
+
+ // Gather plugin distribution URL
+ $url = $metadata['dist']['url'];
+ $filename = array_last(explode('/', $url));
+ $plugins_dir = $manager->getPluginsDir();
+ $tmp_path = $plugins_dir.DIRECTORY_SEPARATOR.$filename;
+ $client = new \GuzzleHttp\Client();
+
+ // Download
+ try {
+ $client->request('GET', $url, [
+ 'headers' => ['User-Agent' => config('secure.user_agent')],
+ 'verify' => config('secure.certificates'),
+ 'sink' => $tmp_path
+ ]);
+ } catch (Exception $e) {
+ report($e);
+ return json(trans('admin.plugins.market.download-failed', ['error' => $e->getMessage()]), 2);
+ }
+
+ // Check file's sha1 hash
+ if (sha1_file($tmp_path) !== $metadata['dist']['shasum']) {
+ @unlink($tmp_path);
+ return json(trans('admin.plugins.market.shasum-failed'), 3);
+ }
+
+ // Unzip
+ $zip = new ZipArchive();
+ $res = $zip->open($tmp_path);
+
+ if ($res === true) {
+ if ($zip->extractTo($plugins_dir) === false) {
+ return json(trans('admin.plugins.market.unzip-failed', ['error' => 'Unable to extract the file.']), 4);
+ }
+ } else {
+ return json(trans('admin.plugins.market.unzip-failed', ['error' => $res]), 4);
+ }
+ $zip->close();
+ @unlink($tmp_path);
+
+ return json(trans('admin.plugins.market.install-success'), 0);
+ }
+
+ protected function getPluginMetadata($name)
+ {
+ return collect($this->getAllAvailablePlugins())->where('name', $name)->first();
+ }
+
+ protected function getAllAvailablePlugins()
+ {
+ if (! $this->registryCache) {
+ $client = new \GuzzleHttp\Client();
+
+ try {
+ $pluginsJson = $client->request('GET', config('plugins.registry'), [
+ 'headers' => ['User-Agent' => config('secure.user_agent')],
+ 'verify' => config('secure.certificates')
+ ])->getBody();
+ } catch (Exception $e) {
+ throw new Exception(trans('admin.plugins.market.connection-error', [
+ 'error' => htmlentities($e->getMessage())
+ ]));
+ }
+
+ $this->registryCache = json_decode($pluginsJson, true);
+ }
+
+ return array_get($this->registryCache, 'packages', []);
+ }
+}
diff --git a/app/Http/Controllers/PluginController.php b/app/Http/Controllers/PluginController.php
index f4eaca94..349f230b 100644
--- a/app/Http/Controllers/PluginController.php
+++ b/app/Http/Controllers/PluginController.php
@@ -11,16 +11,6 @@ use App\Services\PluginManager;
class PluginController extends Controller
{
- /**
- * @codeCoverageIgnore
- */
- public function showMarket()
- {
- return redirect('/')->setTargetUrl(
- 'https://github.com/printempw/blessing-skin-server/wiki/Plugins'
- );
- }
-
public function showManage()
{
return view('admin.plugins');
diff --git a/app/Services/PluginManager.php b/app/Services/PluginManager.php
index 1b0ad671..2698429f 100644
--- a/app/Services/PluginManager.php
+++ b/app/Services/PluginManager.php
@@ -277,20 +277,24 @@ class PluginManager
/**
* Get the unsatisfied requirements of plugin.
*
- * @param string|Plugin $plugin
+ * @param string|Plugin|array $plugin
* @return array
*/
public function getUnsatisfiedRequirements($plugin)
{
- if (! $plugin instanceof Plugin) {
- $plugin = $this->getPlugin($plugin);
- }
+ if (is_array($plugin)) {
+ $requirements = $plugin;
+ } else {
+ if (! $plugin instanceof Plugin) {
+ $plugin = $this->getPlugin($plugin);
+ }
- if (! $plugin) {
- throw new \InvalidArgumentException('Plugin with given name does not exist.');
- }
+ if (! $plugin) {
+ throw new \InvalidArgumentException('Plugin with given name does not exist.');
+ }
- $requirements = $plugin->getRequirements();
+ $requirements = $plugin->getRequirements();
+ }
$unsatisfied = [];
@@ -334,7 +338,7 @@ class PluginManager
/**
* Whether the plugin's requirements are satisfied.
*
- * @param string|Plugin $plugin
+ * @param string|Plugin|array $plugin
* @return bool
*/
public function isRequirementsSatisfied($plugin)
@@ -347,7 +351,7 @@ class PluginManager
*
* @return string
*/
- protected function getPluginsDir()
+ public function getPluginsDir()
{
return config('plugins.directory') ?: base_path('plugins');
}
diff --git a/config/plugins.php b/config/plugins.php
index bd499d11..16d45eb0 100644
--- a/config/plugins.php
+++ b/config/plugins.php
@@ -22,4 +22,14 @@ return [
|
*/
'url' => menv('PLUGINS_URL'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Plugins Market Source
+ |--------------------------------------------------------------------------
+ |
+ | Specify where to get plugins' metadata for plugin merket.
+ |
+ */
+ 'registry' => menv('PLUGINS_REGISTRY', 'https://coding.net/u/printempw/p/bs-plugins-archive/git/raw/master/plugins.json'),
];
diff --git a/resources/assets/src/js/admin/market.js b/resources/assets/src/js/admin/market.js
new file mode 100644
index 00000000..1b1c6f49
--- /dev/null
+++ b/resources/assets/src/js/admin/market.js
@@ -0,0 +1,155 @@
+'use strict';
+
+if ($('#market-table').length === 1) {
+ $(document).ready(initMarketTable);
+}
+
+function initMarketTable() {
+ $.marketTable = $('#market-table').DataTable({
+ columnDefs: marketTableColumnDefs,
+ ajax: {
+ url: url('admin/plugins/market-data'),
+ type: 'POST'
+ }
+ }).on('xhr.dt', handleDataTablesAjaxError);
+}
+
+const marketTableColumnDefs = [
+ {
+ targets: 0,
+ title: trans('admin.pluginTitle'),
+ data: 'title',
+ render: title => `${ title }`
+ },
+ {
+ targets: 1,
+ title: trans('admin.pluginDescription'),
+ data: 'description',
+ orderable: false
+ },
+ {
+ targets: 2,
+ title: trans('admin.pluginAuthor'),
+ data: 'author',
+ orderable: false
+ },
+ {
+ targets: 3,
+ title: trans('admin.pluginVersion'),
+ data: 'version',
+ orderable: false
+ },
+ {
+ targets: 4,
+ data: 'dependencies',
+ title: trans('admin.pluginDependencies'),
+ searchable: false,
+ orderable: false,
+ render: data => {
+ if (data.requirements.length === 0) {
+ return `${trans('admin.noDependencies')}`;
+ }
+
+ let result = '';
+
+ for (const name in data.requirements) {
+ const constraint = data.requirements[name];
+ const color = (name in data.unsatisfiedRequirements) ? 'red' : 'green';
+
+ result += `${name}: ${constraint}
`;
+ }
+
+ return result;
+ }
+ },
+ {
+ targets: 5,
+ title: trans('admin.pluginOperations'),
+ orderable: false,
+ render: (data, type, row) => {
+ if (row.installed) {
+ if (row.update_available) {
+ return `
+
+ `;
+ }
+ if (row.enabled) {
+ return `
+
+ `;
+ }
+ return `
+
+ `;
+ }
+ return `
+
+ `;
+ }
+ }
+];
+
+async function installPlugin(name, option = {}) {
+ const button = $(`#plugin-${name} .btn`);
+ const originalBtnText = button.html();
+
+ try {
+ const { errno, msg } = await fetch($.extend(true, {
+ type: 'POST',
+ url: url('admin/plugins/market/download'),
+ dataType: 'json',
+ data: { name },
+ beforeSend: () => {
+ button.html(` ${ trans('admin.pluginInstalling') }`).prop('disabled', true);
+ }
+ }, option));
+
+ if (errno === 0) {
+ toastr.success(msg);
+
+ $.marketTable.ajax.reload(null, false);
+ } else {
+ button.html(originalBtnText).prop('disabled', false);
+ swal({ type: 'warning', html: msg });
+ }
+ } catch (error) {
+ button.html(originalBtnText).prop('disabled', false);
+ showAjaxError(error);
+ }
+}
+
+async function updatePlugin(name) {
+ const data = $.marketTable.row(`#plugin-${name}`).data();
+
+ if (data.installed === false) {
+ swal({ type: 'warning', html: 'not installed' });
+ }
+
+ await swal({
+ text: trans('admin.confirmUpdate', { plugin: data.title, old: data.installed, new: data.version }),
+ type: 'warning',
+ showCancelButton: true
+ });
+
+ installPlugin(name, {
+ beforeSend: () => {
+ $(`#plugin-${name} .btn`).html(` ${ trans('admin.pluginUpdating') }`).prop('disabled', true);
+ }
+ });
+}
+
+if (process.env.NODE_ENV === 'test') {
+ module.exports = {
+ initMarketTable,
+ installPlugin,
+ updatePlugin,
+ };
+}
diff --git a/resources/assets/src/stylus/admin.styl b/resources/assets/src/stylus/admin.styl
index d21400fd..ef269121 100644
--- a/resources/assets/src/stylus/admin.styl
+++ b/resources/assets/src/stylus/admin.styl
@@ -67,6 +67,7 @@ td {
td:first-child {
border-left: 3px solid transparent;
+ white-space: nowrap;
}
.plugin-enabled {
@@ -77,3 +78,13 @@ td {
border-left-color: #3c8dbc;
}
}
+
+#market-table {
+ .btn {
+ float: right;
+ }
+
+ td:first-child {
+ white-space: nowrap;
+ }
+}
diff --git a/resources/lang/en/admin.yml b/resources/lang/en/admin.yml
index 9230bddd..9aa9eb69 100644
--- a/resources/lang/en/admin.yml
+++ b/resources/lang/en/admin.yml
@@ -98,11 +98,6 @@ customize:
black-light: Black Light
plugins:
- status:
- title: Status
- enabled: Enabled
- disabled: Disabled
-
operations:
title: Operations
enabled: :plugin has been enabled.
@@ -115,6 +110,14 @@ plugins:
no-config-notice: The plugin is not installed or doesn't provide configuration page.
not-found: No such plugin.
+ market:
+ connection-error: Unable to connect to the plugins registry. :error
+ non-existent: The plugin :plugin does not exist.
+ download-failed: Unable to download the plugin. :error
+ shasum-failed: The downloaded file failed hash check, please retry.
+ unzip-failed: Unable to extract the plugin. :error
+ install-success: Plugin was installed.
+
empty: No result
update:
diff --git a/resources/lang/en/locale.js b/resources/lang/en/locale.js
index a77d3511..53f0e30a 100644
--- a/resources/lang/en/locale.js
+++ b/resources/lang/en/locale.js
@@ -181,13 +181,20 @@
pluginTitle: 'Plugin',
pluginAuthor: 'Author',
pluginVersion: 'Version',
+ pluginOperations: 'Operations',
+ pluginDescription: 'Description',
+ pluginDependencies: 'Dependencies',
+ pluginEnabled: 'Enabled',
enablePlugin: 'Enable',
disablePlugin: 'Disable',
configurePlugin: 'Configure',
+ installPlugin: 'Install',
+ pluginInstalling: 'Installing...',
+ updatePlugin: 'Update',
+ pluginUpdating: 'Updating...',
+ confirmUpdate: 'Are you sure to update ":plugin" from :old to :new?',
deletePlugin: 'Delete',
confirmDeletion: 'Are you sure to delete this plugin?',
- pluginDescription: 'Description',
- pluginDependencies: 'Dependencies',
noDependencies: 'No Dependencies',
whyDependencies: 'What\'s this?',
noDependenciesNotice: 'There is no dependency definition in the plugin. It means that the plugin may be not compatible with the current version of Blessing Skin, and enabling it may cause unexpected problems. Do you really want to enable the plugin?',
diff --git a/resources/lang/zh_CN/admin.yml b/resources/lang/zh_CN/admin.yml
index d747f430..9bda3adf 100644
--- a/resources/lang/zh_CN/admin.yml
+++ b/resources/lang/zh_CN/admin.yml
@@ -109,6 +109,14 @@ plugins:
no-config-notice: 插件未安装或未提供配置页面
not-found: 插件不存在
+ market:
+ connection-error: 无法连接至插件市场源,错误信息::error
+ non-existent: 插件 :plugin 不存在
+ download-failed: 插件下载失败,错误信息::error
+ shasum-failed: 文件校验失败,请尝试重新下载
+ unzip-failed: 插件解压缩失败,错误信息::error
+ install-success: 插件安装成功
+
empty: 无结果
update:
diff --git a/resources/lang/zh_CN/locale.js b/resources/lang/zh_CN/locale.js
index f71ba55b..d21ebc33 100644
--- a/resources/lang/zh_CN/locale.js
+++ b/resources/lang/zh_CN/locale.js
@@ -183,13 +183,20 @@
pluginTitle: '插件',
pluginAuthor: '作者',
pluginVersion: '版本',
+ pluginOperations: '操作',
+ pluginDescription: '描述',
+ pluginDependencies: '依赖关系',
+ pluginEnabled: '已启用',
enablePlugin: '启用',
disablePlugin: '禁用',
configurePlugin: '配置',
+ installPlugin: '安装',
+ pluginInstalling: '正在安装...',
+ updatePlugin: '更新',
+ pluginUpdating: '正在更新...',
+ confirmUpdate: '确定将「:plugin」从 :old 升级至 :new?',
deletePlugin: '删除',
confirmDeletion: '真的要删除这个插件吗?',
- pluginDescription: '描述',
- pluginDependencies: '依赖关系',
noDependencies: '无要求',
whyDependencies: '为什么会这样?',
noDependenciesNotice: '此插件没有声明任何依赖关系,这代表它有可能并不兼容此版本的 Blessing Skin,请将此插件升级至可能的最新版本。强行启用可能导致无法预料的后果。你确定要启用此插件吗?',
diff --git a/resources/views/admin/market.tpl b/resources/views/admin/market.tpl
new file mode 100644
index 00000000..df8da6b9
--- /dev/null
+++ b/resources/views/admin/market.tpl
@@ -0,0 +1,26 @@
+@extends('admin.master')
+
+@section('title', trans('general.plugin-market'))
+
+@section('content')
+
+
+