webman/plugin/admin/app/controller/PluginController.php

549 lines
16 KiB
PHP

<?php
namespace plugin\admin\app\controller;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use plugin\admin\app\common\Util;
use process\Monitor;
use support\exception\BusinessException;
use support\Log;
use support\Request;
use support\Response;
use ZIPARCHIVE;
use function array_diff;
use function ini_get;
use function scandir;
use const DIRECTORY_SEPARATOR;
use const PATH_SEPARATOR;
class PluginController extends Base
{
/**
* 不需要鉴权的方法
* @var string[]
*/
protected $noNeedAuth = ['schema', 'captcha'];
/**
* @param Request $request
* @return string
* @throws GuzzleException
*/
public function index(Request $request)
{
$client = $this->httpClient();
$response = $client->get("/webman-admin/apps");
return (string)$response->getBody();
}
/**
* 列表
* @param Request $request
* @return Response
* @throws GuzzleException
*/
public function list(Request $request): Response
{
$installed = $this->getLocalPlugins();
$client = $this->httpClient();
$query = $request->get();
$query['version'] = $this->getAdminVersion();
$response = $client->get('/api/app/list', ['query' => $query]);
$content = $response->getBody()->getContents();
$data = json_decode($content, true);
if (!$data) {
$msg = "/api/app/list return $content";
echo "msg\r\n";
Log::error($msg);
return $this->json(1, '获取数据出错');
}
$disabled = is_phar();
foreach ($data['data']['items'] as $key => $item) {
$name = $item['name'];
$data['data']['items'][$key]['installed'] = $installed[$name] ?? 0;
$data['data']['items'][$key]['disabled'] = $disabled;
}
$items = $data['data']['items'];
$count = $data['data']['total'];
return json(['code' => 0, 'msg' => 'ok', 'data' => $items, 'count' => $count]);
}
/**
* 安装
* @param Request $request
* @return Response
* @throws GuzzleException|BusinessException
*/
public function install(Request $request): Response
{
$name = $request->post('name');
$version = $request->post('version');
$installed_version = $this->getPluginVersion($name);
if (!$name || !$version) {
return $this->json(1, '缺少参数');
}
$user = session('app-plugin-user');
if (!$user) {
return $this->json(-1, '请登录');
}
// 获取下载zip文件url
$data = $this->getDownloadUrl($name, $version);
if ($data['code'] != 0) {
return $this->json($data['code'], $data['msg'], $data['data'] ?? []);
}
// 下载zip文件
$base_path = base_path() . "/plugin/$name";
$zip_file = "$base_path.zip";
$extract_to = base_path() . '/plugin/';
$this->downloadZipFile($data['data']['url'], $zip_file);
$has_zip_archive = class_exists(ZipArchive::class, false);
if (!$has_zip_archive) {
$cmd = $this->getUnzipCmd($zip_file, $extract_to);
if (!$cmd) {
throw new BusinessException('请给php安装zip模块或者给系统安装unzip命令');
}
if (!function_exists('proc_open')) {
throw new BusinessException('请解除proc_open函数的禁用或者给php安装zip模块');
}
}
Util::pauseFileMonitor();
try {
// 解压zip到plugin目录
if ($has_zip_archive) {
$zip = new ZipArchive;
$zip->open($zip_file);
}
$context = null;
$install_class = "\\plugin\\$name\\api\\Install";
if ($installed_version) {
// 执行beforeUpdate
if (class_exists($install_class) && method_exists($install_class, 'beforeUpdate')) {
$context = call_user_func([$install_class, 'beforeUpdate'], $installed_version, $version);
}
}
if (!empty($zip)) {
$zip->extractTo(base_path() . '/plugin/');
unset($zip);
} else {
$this->unzipWithCmd($cmd);
}
unlink($zip_file);
if ($installed_version) {
// 执行update更新
if (class_exists($install_class) && method_exists($install_class, 'update')) {
call_user_func([$install_class, 'update'], $installed_version, $version, $context);
}
} else {
// 执行install安装
if (class_exists($install_class) && method_exists($install_class, 'install')) {
call_user_func([$install_class, 'install'], $version);
}
}
} finally {
Util::resumeFileMonitor();
}
Util::reloadWebman();
return $this->json(0);
}
/**
* 卸载
* @param Request $request
* @return Response
*/
public function uninstall(Request $request): Response
{
$name = $request->post('name');
$version = $request->post('version');
if (!$name || !preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
return $this->json(1, '参数错误');
}
// 获得插件路径
clearstatcache();
$path = get_realpath(base_path() . "/plugin/$name");
if (!$path || !is_dir($path)) {
return $this->json(1, '已经删除');
}
// 执行uninstall卸载
$install_class = "\\plugin\\$name\\api\\Install";
if (class_exists($install_class) && method_exists($install_class, 'uninstall')) {
call_user_func([$install_class, 'uninstall'], $version);
}
// 删除目录
clearstatcache();
if (is_dir($path)) {
$monitor_support_pause = method_exists(Monitor::class, 'pause');
if ($monitor_support_pause) {
Monitor::pause();
}
try {
$this->rmDir($path);
} finally {
if ($monitor_support_pause) {
Monitor::resume();
}
}
}
clearstatcache();
Util::reloadWebman();
return $this->json(0);
}
/**
* 支付
* @param Request $request
* @return string|Response
* @throws GuzzleException
*/
public function pay(Request $request)
{
$app = $request->get('app');
if (!$app) {
return response('app not found');
}
$token = session('app-plugin-token');
if (!$token) {
return 'Please login workerman.net';
}
$client = $this->httpClient();
$response = $client->get("/payment/app/$app/$token");
return (string)$response->getBody();
}
/**
* 登录验证码
* @param Request $request
* @return Response
* @throws GuzzleException
*/
public function captcha(Request $request): Response
{
$client = $this->httpClient();
$response = $client->get('/user/captcha?type=login');
$sid_str = $response->getHeaderLine('Set-Cookie');
if (preg_match('/PHPSID=([a-zA-Z_0-9]+?);/', $sid_str, $match)) {
$sid = $match[1];
session()->set('app-plugin-token', $sid);
}
return response($response->getBody()->getContents())->withHeader('Content-Type', 'image/jpeg');
}
/**
* 登录官网
* @param Request $request
* @return Response|string
* @throws GuzzleException
*/
public function login(Request $request)
{
$client = $this->httpClient();
if ($request->method() === 'GET') {
$response = $client->get("/webman-admin/login");
return (string)$response->getBody();
}
$response = $client->post('/api/user/login', [
'form_params' => [
'email' => $request->post('username'),
'password' => $request->post('password'),
'captcha' => $request->post('captcha')
]
]);
$content = $response->getBody()->getContents();
$data = json_decode($content, true);
if (!$data) {
$msg = "/api/user/login return $content";
echo "msg\r\n";
Log::error($msg);
return $this->json(1, '发生错误');
}
if ($data['code'] != 0) {
return $this->json($data['code'], $data['msg']);
}
session()->set('app-plugin-user', [
'uid' => $data['data']['uid']
]);
return $this->json(0);
}
/**
* 获取zip下载url
* @param $name
* @param $version
* @return mixed
* @throws BusinessException
* @throws GuzzleException
*/
protected function getDownloadUrl($name, $version)
{
$client = $this->httpClient();
$response = $client->get("/app/download/$name?version=$version");
$content = $response->getBody()->getContents();
$data = json_decode($content, true);
if (!$data) {
$msg = "/api/app/download return $content";
Log::error($msg);
throw new BusinessException('访问官方接口失败 ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
}
if ($data['code'] && $data['code'] != -1 && $data['code'] != -2) {
throw new BusinessException($data['msg']);
}
if ($data['code'] == 0 && !isset($data['data']['url'])) {
throw new BusinessException('官方接口返回数据错误');
}
return $data;
}
/**
* 下载zip
* @param $url
* @param $file
* @return void
* @throws BusinessException
* @throws GuzzleException
*/
protected function downloadZipFile($url, $file)
{
$client = $this->downloadClient();
$response = $client->get($url);
$body = $response->getBody();
$status = $response->getStatusCode();
if ($status == 404) {
throw new BusinessException('安装包不存在');
}
$zip_content = $body->getContents();
if (empty($zip_content)) {
throw new BusinessException('安装包不存在');
}
file_put_contents($file, $zip_content);
}
/**
* 获取系统支持的解压命令
* @param $zip_file
* @param $extract_to
* @return mixed|string|null
*/
protected function getUnzipCmd($zip_file, $extract_to)
{
if ($cmd = $this->findCmd('unzip')) {
$cmd = "$cmd -o -qq $zip_file -d $extract_to";
} else if ($cmd = $this->findCmd('7z')) {
$cmd = "$cmd x -bb0 -y $zip_file -o$extract_to";
} else if ($cmd = $this->findCmd('7zz')) {
$cmd = "$cmd x -bb0 -y $zip_file -o$extract_to";
}
return $cmd;
}
/**
* 使用解压命令解压
* @param $cmd
* @return void
* @throws BusinessException
*/
protected function unzipWithCmd($cmd)
{
$desc = [
0 => ["pipe", "r"],
1 => ["pipe", "w"],
2 => ["pipe", "w"],
];
$handler = proc_open($cmd, $desc, $pipes);
if (!is_resource($handler)) {
throw new BusinessException("解压zip时出错:proc_open调用失败");
}
$err = fread($pipes[2], 1024);
fclose($pipes[2]);
proc_close($handler);
if ($err) {
throw new BusinessException("解压zip时出错:$err");
}
}
/**
* 获取已安装的插件列表
* @return array
*/
protected function getLocalPlugins(): array
{
clearstatcache();
$installed = [];
$plugin_names = array_diff(scandir(base_path() . '/plugin/'), array('.', '..')) ?: [];
foreach ($plugin_names as $plugin_name) {
if (is_dir(base_path() . "/plugin/$plugin_name") && $version = $this->getPluginVersion($plugin_name)) {
$installed[$plugin_name] = $version;
}
}
return $installed;
}
/**
* 获取已安装的插件列表
* @param Request $request
* @return Response
*/
public function getInstalledPlugins(Request $request): Response
{
return $this->json(0, 'ok', $this->getLocalPlugins());
}
/**
* 获取本地插件版本
* @param $name
* @return array|mixed|null
*/
protected function getPluginVersion($name)
{
if (!is_file($file = base_path() . "/plugin/$name/config/app.php")) {
return null;
}
$config = include $file;
return $config['version'] ?? null;
}
/**
* 获取webman/admin版本
* @return string
*/
protected function getAdminVersion(): string
{
return config('plugin.admin.app.version', '');
}
/**
* 删除目录
* @param $src
* @return void
*/
protected function rmDir($src)
{
$dir = opendir($src);
while (false !== ($file = readdir($dir))) {
if (($file != '.') && ($file != '..')) {
$full = $src . '/' . $file;
if (is_dir($full)) {
$this->rmDir($full);
} else {
unlink($full);
}
}
}
closedir($dir);
rmdir($src);
}
/**
* 获取httpclient
* @return Client
*/
protected function httpClient(): Client
{
// 下载zip
$options = [
'base_uri' => config('plugin.admin.app.plugin_market_host'),
'timeout' => 60,
'connect_timeout' => 5,
'verify' => false,
'http_errors' => false,
'headers' => [
'Referer' => \request()->fullUrl(),
'User-Agent' => 'webman-app-plugin',
'Accept' => 'application/json;charset=UTF-8',
]
];
if ($token = session('app-plugin-token')) {
$options['headers']['Cookie'] = "PHPSID=$token;";
}
return new Client($options);
}
/**
* 获取下载httpclient
* @return Client
*/
protected function downloadClient(): Client
{
// 下载zip
$options = [
'timeout' => 59,
'connect_timeout' => 5,
'verify' => false,
'http_errors' => false,
'headers' => [
'Referer' => \request()->fullUrl(),
'User-Agent' => 'webman-app-plugin',
]
];
if ($token = session('app-plugin-token')) {
$options['headers']['Cookie'] = "PHPSID=$token;";
}
return new Client($options);
}
/**
* 查找系统命令
* @param string $name
* @param string|null $default
* @param array $extraDirs
* @return mixed|string|null
*/
protected function findCmd(string $name, string $default = null, array $extraDirs = [])
{
if (ini_get('open_basedir')) {
$searchPath = array_merge(explode(PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs);
$dirs = [];
foreach ($searchPath as $path) {
if (@is_dir($path)) {
$dirs[] = $path;
} else {
if (basename($path) == $name && @is_executable($path)) {
return $path;
}
}
}
} else {
$dirs = array_merge(
explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
$extraDirs
);
}
$suffixes = [''];
if ('\\' === DIRECTORY_SEPARATOR) {
$pathExt = getenv('PATHEXT');
$suffixes = array_merge($pathExt ? explode(PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com'], $suffixes);
}
foreach ($suffixes as $suffix) {
foreach ($dirs as $dir) {
if (@is_file($file = $dir . DIRECTORY_SEPARATOR . $name . $suffix) && ('\\' === DIRECTORY_SEPARATOR || @is_executable($file))) {
return $file;
}
}
}
return $default;
}
}