<?php
|
|
namespace think\addons;
|
|
use fast\Http;
|
use GuzzleHttp\Client;
|
use GuzzleHttp\Exception\TransferException;
|
use GuzzleHttp\TransferStats;
|
use PhpZip\Exception\ZipException;
|
use PhpZip\ZipFile;
|
use RecursiveDirectoryIterator;
|
use RecursiveIteratorIterator;
|
use Symfony\Component\VarExporter\VarExporter;
|
use think\Cache;
|
use think\Db;
|
use think\Exception;
|
use think\Log;
|
|
/**
|
* 插件服务
|
* @package think\addons
|
*/
|
class Service
|
{
|
/**
|
* 插件列表
|
*/
|
public static function addons($params = [])
|
{
|
$params['domain'] = request()->host(true);
|
return self::sendRequest('/addon/index', $params, 'GET');
|
}
|
|
/**
|
* 检测插件是否购买授权
|
*/
|
public static function isBuy($name, $extend = [])
|
{
|
$params = array_merge(['name' => $name, 'domain' => request()->host(true)], $extend);
|
return self::sendRequest('/addon/isbuy', $params, 'POST');
|
}
|
|
/**
|
* 检测插件是否授权
|
*
|
* @param string $name 插件名称
|
* @param string $domain 验证域名
|
*/
|
public static function isAuthorization($name, $domain = '')
|
{
|
$config = self::config($name);
|
$request = request();
|
$domain = self::getRootDomain($domain ? $domain : $request->host(true));
|
if (isset($config['domains']) && isset($config['domains']) && isset($config['validations']) && isset($config['licensecodes'])) {
|
$index = array_search($domain, $config['domains']);
|
if ((in_array($domain, $config['domains']) && in_array(md5(md5($domain) . ($config['licensecodes'][$index] ?? '')), $config['validations'])) || $request->isCli()) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
/**
|
* 远程下载插件
|
*
|
* @param string $name 插件名称
|
* @param array $extend 扩展参数
|
* @return string
|
*/
|
public static function download($name, $extend = [])
|
{
|
$addonsTempDir = self::getAddonsBackupDir();
|
$tmpFile = $addonsTempDir . $name . ".zip";
|
try {
|
$client = self::getClient();
|
$response = $client->get('/addon/download', ['query' => array_merge(['name' => $name], $extend)]);
|
$body = $response->getBody();
|
$content = $body->getContents();
|
if (substr($content, 0, 1) === '{') {
|
$json = (array)json_decode($content, true);
|
//如果传回的是一个下载链接,则再次下载
|
if ($json['data'] && isset($json['data']['url'])) {
|
$response = $client->get($json['data']['url']);
|
$body = $response->getBody();
|
$content = $body->getContents();
|
} else {
|
//下载返回错误,抛出异常
|
throw new AddonException($json['msg'], $json['code'], $json['data']);
|
}
|
}
|
} catch (TransferException $e) {
|
throw new Exception("Addon package download failed");
|
}
|
|
if ($write = fopen($tmpFile, 'w')) {
|
fwrite($write, $content);
|
fclose($write);
|
return $tmpFile;
|
}
|
throw new Exception("No permission to write temporary files");
|
}
|
|
/**
|
* 解压插件
|
*
|
* @param string $name 插件名称
|
* @return string
|
* @throws Exception
|
*/
|
public static function unzip($name)
|
{
|
if (!$name) {
|
throw new Exception('Invalid parameters');
|
}
|
$addonsBackupDir = self::getAddonsBackupDir();
|
$file = $addonsBackupDir . $name . '.zip';
|
|
// 打开插件压缩包
|
$zip = new ZipFile();
|
try {
|
$zip->openFile($file);
|
} catch (ZipException $e) {
|
$zip->close();
|
throw new Exception('Unable to open the zip file');
|
}
|
|
$dir = self::getAddonDir($name);
|
if (!is_dir($dir)) {
|
@mkdir($dir, 0755);
|
}
|
|
// 解压插件压缩包
|
try {
|
$zip->extractTo($dir);
|
} catch (ZipException $e) {
|
throw new Exception('Unable to extract the file');
|
} finally {
|
$zip->close();
|
}
|
return $dir;
|
}
|
|
/**
|
* 离线安装
|
* @param string $file 插件压缩包
|
* @param array $extend
|
*/
|
public static function local($file, $extend = [])
|
{
|
$addonsTempDir = self::getAddonsBackupDir();
|
if (!$file || !$file instanceof \think\File) {
|
throw new Exception('No file upload or server upload limit exceeded');
|
}
|
$uploadFile = $file->rule('uniqid')->validate(['size' => 102400000, 'ext' => 'zip,fastaddon'])->move($addonsTempDir);
|
if (!$uploadFile) {
|
// 上传失败获取错误信息
|
throw new Exception(__($file->getError()));
|
}
|
$tmpFile = $addonsTempDir . $uploadFile->getSaveName();
|
|
$info = [];
|
$zip = new ZipFile();
|
try {
|
|
// 打开插件压缩包
|
try {
|
$zip->openFile($tmpFile);
|
} catch (ZipException $e) {
|
@unlink($tmpFile);
|
throw new Exception('Unable to open the zip file');
|
}
|
|
$config = self::getInfoIni($zip);
|
|
// 判断插件标识
|
$name = isset($config['name']) ? $config['name'] : '';
|
if (!$name) {
|
throw new Exception('Addon info file data incorrect');
|
}
|
|
// 判断插件是否存在
|
if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
|
throw new Exception('Addon name incorrect');
|
}
|
|
// 判断新插件是否存在
|
$newAddonDir = self::getAddonDir($name);
|
if (is_dir($newAddonDir)) {
|
throw new Exception('Addon already exists');
|
}
|
|
// 追加MD5和Data数据
|
$extend['md5'] = md5_file($tmpFile);
|
$extend['data'] = $zip->getArchiveComment();
|
$extend['unknownsources'] = config('app_debug') && config('fastadmin.unknownsources');
|
$extend['faversion'] = config('fastadmin.version');
|
|
$params = array_merge($config, $extend);
|
|
// 压缩包验证、版本依赖判断
|
Service::valid($params);
|
|
//创建插件目录
|
@mkdir($newAddonDir, 0755, true);
|
|
// 解压到插件目录
|
try {
|
$zip->extractTo($newAddonDir);
|
} catch (ZipException $e) {
|
@unlink($newAddonDir);
|
throw new Exception('Unable to extract the file');
|
}
|
|
Db::startTrans();
|
try {
|
//默认禁用该插件
|
$info = get_addon_info($name);
|
if ($info['state']) {
|
$info['state'] = 0;
|
set_addon_info($name, $info);
|
}
|
|
//执行插件的安装方法
|
$class = get_addon_class($name);
|
if (class_exists($class)) {
|
$addon = new $class();
|
$addon->install();
|
}
|
Db::commit();
|
} catch (\Exception $e) {
|
Db::rollback();
|
@rmdirs($newAddonDir);
|
throw new Exception(__($e->getMessage()));
|
}
|
|
//导入SQL
|
Service::importsql($name);
|
} catch (AddonException $e) {
|
throw new AddonException($e->getMessage(), $e->getCode(), $e->getData());
|
} catch (Exception $e) {
|
throw new Exception(__($e->getMessage()));
|
} finally {
|
$zip->close();
|
unset($uploadFile);
|
@unlink($tmpFile);
|
}
|
|
$info['config'] = get_addon_config($name) ? 1 : 0;
|
$info['bootstrap'] = is_file(Service::getBootstrapFile($name));
|
$info['testdata'] = is_file(Service::getTestdataFile($name));
|
return $info;
|
}
|
|
/**
|
* 验证压缩包、依赖验证
|
* @param array $params
|
* @return bool
|
* @throws Exception
|
*/
|
public static function valid($params = [])
|
{
|
$json = self::sendRequest('/addon/valid', $params, 'POST');
|
if ($json && isset($json['code'])) {
|
if ($json['code']) {
|
return true;
|
} else {
|
throw new Exception($json['msg'] ?? "Invalid addon package");
|
}
|
} else {
|
throw new Exception("Unknown data format");
|
}
|
}
|
|
/**
|
* 备份插件
|
* @param string $name 插件名称
|
* @return bool
|
* @throws Exception
|
*/
|
public static function backup($name)
|
{
|
$addonsBackupDir = self::getAddonsBackupDir();
|
$file = $addonsBackupDir . $name . '-backup-' . date("YmdHis") . '.zip';
|
$zipFile = new ZipFile();
|
try {
|
$zipFile
|
->addDirRecursive(self::getAddonDir($name))
|
->saveAsFile($file)
|
->close();
|
} catch (ZipException $e) {
|
|
} finally {
|
$zipFile->close();
|
}
|
|
return true;
|
}
|
|
/**
|
* 检测插件是否完整
|
*
|
* @param string $name 插件名称
|
* @return boolean
|
* @throws Exception
|
*/
|
public static function check($name)
|
{
|
if (!$name || !is_dir(ADDON_PATH . $name)) {
|
throw new Exception('Addon not exists');
|
}
|
$addonClass = get_addon_class($name);
|
if (!$addonClass) {
|
throw new Exception("The addon file does not exist");
|
}
|
$addon = new $addonClass();
|
if (!$addon->checkInfo()) {
|
throw new Exception("The configuration file content is incorrect");
|
}
|
return true;
|
}
|
|
/**
|
* 是否有冲突
|
*
|
* @param string $name 插件名称
|
* @return boolean
|
* @throws AddonException
|
*/
|
public static function noconflict($name)
|
{
|
// 检测冲突文件
|
$list = self::getGlobalFiles($name, true);
|
if ($list) {
|
//发现冲突文件,抛出异常
|
throw new AddonException(__("Conflicting file found"), -3, ['conflictlist' => $list]);
|
}
|
return true;
|
}
|
|
/**
|
* 导入SQL
|
*
|
* @param string $name 插件名称
|
* @param string $fileName SQL文件名称
|
* @return boolean
|
*/
|
public static function importsql($name, $fileName = null)
|
{
|
$fileName = is_null($fileName) ? 'install.sql' : $fileName;
|
$sqlFile = self::getAddonDir($name) . $fileName;
|
if (is_file($sqlFile)) {
|
$lines = file($sqlFile);
|
$templine = '';
|
foreach ($lines as $line) {
|
if (substr($line, 0, 2) == '--' || $line == '' || substr($line, 0, 2) == '/*') {
|
continue;
|
}
|
|
$templine .= $line;
|
if (substr(trim($line), -1, 1) == ';') {
|
$templine = str_ireplace('__PREFIX__', config('database.prefix'), $templine);
|
$templine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $templine);
|
try {
|
Db::getPdo()->exec($templine);
|
} catch (\PDOException $e) {
|
//$e->getMessage();
|
}
|
$templine = '';
|
}
|
}
|
}
|
return true;
|
}
|
|
/**
|
* 刷新插件缓存文件
|
*
|
* @return boolean
|
* @throws Exception
|
*/
|
public static function refresh()
|
{
|
//刷新addons.js
|
$addons = get_addon_list();
|
$bootstrapArr = [];
|
foreach ($addons as $name => $addon) {
|
$bootstrapFile = self::getBootstrapFile($name);
|
if ($addon['state'] && is_file($bootstrapFile)) {
|
$bootstrapArr[] = file_get_contents($bootstrapFile);
|
}
|
}
|
$addonsFile = ROOT_PATH . str_replace("/", DS, "public/assets/js/addons.js");
|
if ($handle = fopen($addonsFile, 'w')) {
|
$tpl = <<<EOD
|
define([], function () {
|
{__JS__}
|
});
|
EOD;
|
fwrite($handle, str_replace("{__JS__}", implode("\n", $bootstrapArr), $tpl));
|
fclose($handle);
|
} else {
|
throw new Exception(__("Unable to open file '%s' for writing", "addons.js"));
|
}
|
|
Cache::rm("addons");
|
Cache::rm("hooks");
|
|
$file = self::getExtraAddonsFile();
|
|
$config = get_addon_autoload_config(true);
|
if ($config['autoload']) {
|
return;
|
}
|
|
if (!is_really_writable($file)) {
|
throw new Exception(__("Unable to open file '%s' for writing", "addons.php"));
|
}
|
|
file_put_contents($file, "<?php\n\n" . "return " . VarExporter::export($config) . ";\n", LOCK_EX);
|
return true;
|
}
|
|
/**
|
* 安装插件
|
*
|
* @param string $name 插件名称
|
* @param boolean $force 是否覆盖
|
* @param array $extend 扩展参数
|
* @return boolean
|
* @throws Exception
|
* @throws AddonException
|
*/
|
public static function install($name, $force = false, $extend = [])
|
{
|
if (!$name || (is_dir(ADDON_PATH . $name) && !$force)) {
|
throw new Exception('Addon already exists');
|
}
|
|
$extend['domain'] = request()->host(true);
|
|
// 远程下载插件
|
$tmpFile = Service::download($name, $extend);
|
|
$addonDir = self::getAddonDir($name);
|
|
try {
|
// 解压插件压缩包到插件目录
|
Service::unzip($name);
|
|
// 检查插件是否完整
|
Service::check($name);
|
|
if (!$force) {
|
Service::noconflict($name);
|
}
|
} catch (AddonException $e) {
|
@rmdirs($addonDir);
|
throw new AddonException($e->getMessage(), $e->getCode(), $e->getData());
|
} catch (Exception $e) {
|
@rmdirs($addonDir);
|
throw new Exception($e->getMessage());
|
} finally {
|
// 移除临时文件
|
@unlink($tmpFile);
|
}
|
|
// 默认启用该插件
|
$info = get_addon_info($name);
|
|
Db::startTrans();
|
try {
|
if (!$info['state']) {
|
$info['state'] = 1;
|
set_addon_info($name, $info);
|
}
|
|
// 执行安装脚本
|
$class = get_addon_class($name);
|
if (class_exists($class)) {
|
$addon = new $class();
|
$addon->install();
|
}
|
Db::commit();
|
} catch (Exception $e) {
|
@rmdirs($addonDir);
|
Db::rollback();
|
throw new Exception($e->getMessage());
|
}
|
|
// 导入
|
Service::importsql($name);
|
|
// 启用插件
|
Service::enable($name, true);
|
|
$info['config'] = get_addon_config($name) ? 1 : 0;
|
$info['bootstrap'] = is_file(Service::getBootstrapFile($name));
|
$info['testdata'] = is_file(Service::getTestdataFile($name));
|
return $info;
|
}
|
|
/**
|
* 卸载插件
|
*
|
* @param string $name
|
* @param boolean $force 是否强制卸载
|
* @return boolean
|
* @throws Exception
|
*/
|
public static function uninstall($name, $force = false)
|
{
|
if (!$name || !is_dir(ADDON_PATH . $name)) {
|
throw new Exception('Addon not exists');
|
}
|
|
if (!$force) {
|
Service::noconflict($name);
|
}
|
|
// 移除插件全局资源文件
|
if ($force) {
|
$list = Service::getGlobalFiles($name);
|
foreach ($list as $k => $v) {
|
@unlink(ROOT_PATH . $v);
|
}
|
}
|
|
// 执行卸载脚本
|
try {
|
$class = get_addon_class($name);
|
if (class_exists($class)) {
|
$addon = new $class();
|
$addon->uninstall();
|
}
|
} catch (Exception $e) {
|
throw new Exception($e->getMessage());
|
}
|
|
// 移除插件目录
|
rmdirs(ADDON_PATH . $name);
|
|
// 刷新
|
Service::refresh();
|
return true;
|
}
|
|
/**
|
* 启用
|
* @param string $name 插件名称
|
* @param boolean $force 是否强制覆盖
|
* @return boolean
|
*/
|
public static function enable($name, $force = false)
|
{
|
if (!$name || !is_dir(ADDON_PATH . $name)) {
|
throw new Exception('Addon not exists');
|
}
|
|
if (!$force) {
|
Service::noconflict($name);
|
}
|
|
//备份冲突文件
|
if (config('fastadmin.backup_global_files')) {
|
$conflictFiles = self::getGlobalFiles($name, true);
|
if ($conflictFiles) {
|
$zip = new ZipFile();
|
try {
|
foreach ($conflictFiles as $k => $v) {
|
$zip->addFile(ROOT_PATH . $v, $v);
|
}
|
$addonsBackupDir = self::getAddonsBackupDir();
|
$zip->saveAsFile($addonsBackupDir . $name . "-conflict-enable-" . date("YmdHis") . ".zip");
|
} catch (Exception $e) {
|
|
} finally {
|
$zip->close();
|
}
|
}
|
}
|
|
$addonDir = self::getAddonDir($name);
|
$sourceAssetsDir = self::getSourceAssetsDir($name);
|
$destAssetsDir = self::getDestAssetsDir($name);
|
|
$files = self::getGlobalFiles($name);
|
if ($files) {
|
//刷新插件配置缓存
|
Service::config($name, ['files' => $files]);
|
}
|
|
// 复制文件
|
if (is_dir($sourceAssetsDir)) {
|
copydirs($sourceAssetsDir, $destAssetsDir);
|
}
|
|
// 复制application和public到全局
|
foreach (self::getCheckDirs() as $k => $dir) {
|
if (is_dir($addonDir . $dir)) {
|
copydirs($addonDir . $dir, ROOT_PATH . $dir);
|
}
|
}
|
|
//插件纯净模式时将插件目录下的application、public和assets删除
|
if (config('fastadmin.addon_pure_mode')) {
|
// 删除插件目录已复制到全局的文件
|
@rmdirs($sourceAssetsDir);
|
foreach (self::getCheckDirs() as $k => $dir) {
|
@rmdirs($addonDir . $dir);
|
}
|
}
|
|
//执行启用脚本
|
try {
|
$class = get_addon_class($name);
|
if (class_exists($class)) {
|
$addon = new $class();
|
if (method_exists($class, "enable")) {
|
$addon->enable();
|
}
|
}
|
} catch (Exception $e) {
|
throw new Exception($e->getMessage());
|
}
|
|
$info = get_addon_info($name);
|
$info['state'] = 1;
|
unset($info['url']);
|
|
set_addon_info($name, $info);
|
|
// 刷新
|
Service::refresh();
|
return true;
|
}
|
|
/**
|
* 禁用
|
*
|
* @param string $name 插件名称
|
* @param boolean $force 是否强制禁用
|
* @return boolean
|
* @throws Exception
|
*/
|
public static function disable($name, $force = false)
|
{
|
if (!$name || !is_dir(ADDON_PATH . $name)) {
|
throw new Exception('Addon not exists');
|
}
|
|
$file = self::getExtraAddonsFile();
|
if (!is_really_writable($file)) {
|
throw new Exception(__("Unable to open file '%s' for writing", "addons.php"));
|
}
|
|
if (!$force) {
|
Service::noconflict($name);
|
}
|
|
if (config('fastadmin.backup_global_files')) {
|
//仅备份修改过的文件
|
$conflictFiles = Service::getGlobalFiles($name, true);
|
if ($conflictFiles) {
|
$zip = new ZipFile();
|
try {
|
foreach ($conflictFiles as $k => $v) {
|
$zip->addFile(ROOT_PATH . $v, $v);
|
}
|
$addonsBackupDir = self::getAddonsBackupDir();
|
$zip->saveAsFile($addonsBackupDir . $name . "-conflict-disable-" . date("YmdHis") . ".zip");
|
} catch (Exception $e) {
|
|
} finally {
|
$zip->close();
|
}
|
}
|
}
|
|
$config = Service::config($name);
|
|
$addonDir = self::getAddonDir($name);
|
//插件资源目录
|
$destAssetsDir = self::getDestAssetsDir($name);
|
|
// 移除插件全局文件
|
$list = Service::getGlobalFiles($name);
|
|
//插件纯净模式时将原有的文件复制回插件目录
|
//当无法获取全局文件列表时也将列表复制回插件目录
|
if (config('fastadmin.addon_pure_mode') || !$list) {
|
if ($config && isset($config['files']) && is_array($config['files'])) {
|
foreach ($config['files'] as $index => $item) {
|
//避免切换不同服务器后导致路径不一致
|
$item = str_replace(['/', '\\'], DS, $item);
|
//插件资源目录,无需重复复制
|
if (stripos($item, str_replace(ROOT_PATH, '', $destAssetsDir)) === 0) {
|
continue;
|
}
|
//检查目录是否存在,不存在则创建
|
$itemBaseDir = dirname($addonDir . $item);
|
if (!is_dir($itemBaseDir)) {
|
@mkdir($itemBaseDir, 0755, true);
|
}
|
if (is_file(ROOT_PATH . $item)) {
|
@copy(ROOT_PATH . $item, $addonDir . $item);
|
}
|
}
|
$list = $config['files'];
|
}
|
//复制插件目录资源
|
if (is_dir($destAssetsDir)) {
|
@copydirs($destAssetsDir, $addonDir . 'assets' . DS);
|
}
|
}
|
|
$dirs = [];
|
foreach ($list as $k => $v) {
|
$file = ROOT_PATH . $v;
|
$dirs[] = dirname($file);
|
@unlink($file);
|
}
|
|
// 移除插件空目录
|
$dirs = array_filter(array_unique($dirs));
|
foreach ($dirs as $k => $v) {
|
remove_empty_folder($v);
|
}
|
|
$info = get_addon_info($name);
|
$info['state'] = 0;
|
unset($info['url']);
|
|
set_addon_info($name, $info);
|
|
// 执行禁用脚本
|
try {
|
$class = get_addon_class($name);
|
if (class_exists($class)) {
|
$addon = new $class();
|
|
if (method_exists($class, "disable")) {
|
$addon->disable();
|
}
|
}
|
} catch (Exception $e) {
|
throw new Exception($e->getMessage());
|
}
|
|
// 刷新
|
Service::refresh();
|
return true;
|
}
|
|
/**
|
* 升级插件
|
*
|
* @param string $name 插件名称
|
* @param array $extend 扩展参数
|
*/
|
public static function upgrade($name, $extend = [])
|
{
|
$info = get_addon_info($name);
|
if ($info['state']) {
|
throw new Exception(__('Please disable addon first'));
|
}
|
$config = get_addon_config($name);
|
if ($config) {
|
//备份配置
|
}
|
|
// 远程下载插件
|
$tmpFile = Service::download($name, $extend);
|
|
// 备份插件文件
|
Service::backup($name);
|
|
$addonDir = self::getAddonDir($name);
|
|
// 删除插件目录下的application和public
|
$files = self::getCheckDirs();
|
foreach ($files as $index => $file) {
|
@rmdirs($addonDir . $file);
|
}
|
|
try {
|
// 解压插件
|
Service::unzip($name);
|
} catch (Exception $e) {
|
throw new Exception($e->getMessage());
|
} finally {
|
// 移除临时文件
|
@unlink($tmpFile);
|
}
|
|
if ($config) {
|
// 还原配置
|
set_addon_config($name, $config);
|
}
|
|
// 导入
|
Service::importsql($name);
|
|
// 执行升级脚本
|
try {
|
$addonName = ucfirst($name);
|
//创建临时类用于调用升级的方法
|
$sourceFile = $addonDir . $addonName . ".php";
|
$destFile = $addonDir . $addonName . "Upgrade.php";
|
|
$classContent = str_replace("class {$addonName} extends", "class {$addonName}Upgrade extends", file_get_contents($sourceFile));
|
|
//创建临时的类文件
|
file_put_contents($destFile, $classContent);
|
|
$className = "\\addons\\" . $name . "\\" . $addonName . "Upgrade";
|
$addon = new $className($name);
|
|
//调用升级的方法
|
if (method_exists($addon, "upgrade")) {
|
$addon->upgrade();
|
}
|
|
//移除临时文件
|
@unlink($destFile);
|
} catch (Exception $e) {
|
throw new Exception($e->getMessage());
|
}
|
|
// 刷新
|
Service::refresh();
|
|
//必须变更版本号
|
$info['version'] = isset($extend['version']) ? $extend['version'] : $info['version'];
|
|
$info['config'] = get_addon_config($name) ? 1 : 0;
|
$info['bootstrap'] = is_file(Service::getBootstrapFile($name));
|
return $info;
|
}
|
|
/**
|
* 读取或修改插件配置
|
* @param string $name
|
* @param array $changed
|
* @return array
|
*/
|
public static function config($name, $changed = [])
|
{
|
$addonDir = self::getAddonDir($name);
|
$addonConfigFile = $addonDir . '.addonrc';
|
$config = [];
|
if (is_file($addonConfigFile)) {
|
$config = (array)json_decode(file_get_contents($addonConfigFile), true);
|
}
|
$config = array_merge($config, $changed);
|
if ($changed) {
|
file_put_contents($addonConfigFile, json_encode($config, JSON_UNESCAPED_UNICODE));
|
}
|
return $config;
|
}
|
|
/**
|
* 获取插件在全局的文件
|
*
|
* @param string $name 插件名称
|
* @param boolean $onlyconflict 是否只返回冲突文件
|
* @return array
|
*/
|
public static function getGlobalFiles($name, $onlyconflict = false)
|
{
|
$list = [];
|
$addonDir = self::getAddonDir($name);
|
$checkDirList = self::getCheckDirs();
|
$checkDirList = array_merge($checkDirList, ['assets']);
|
|
$assetDir = self::getDestAssetsDir($name);
|
|
// 扫描插件目录是否有覆盖的文件
|
foreach ($checkDirList as $k => $dirName) {
|
//检测目录是否存在
|
if (!is_dir($addonDir . $dirName)) {
|
continue;
|
}
|
//匹配出所有的文件
|
$files = new RecursiveIteratorIterator(
|
new RecursiveDirectoryIterator($addonDir . $dirName, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST
|
);
|
|
foreach ($files as $fileinfo) {
|
if ($fileinfo->isFile()) {
|
$filePath = $fileinfo->getPathName();
|
//如果名称为assets需要做特殊处理
|
if ($dirName === 'assets') {
|
$path = str_replace(ROOT_PATH, '', $assetDir) . str_replace($addonDir . $dirName . DS, '', $filePath);
|
} else {
|
$path = str_replace($addonDir, '', $filePath);
|
}
|
if ($onlyconflict) {
|
$destPath = ROOT_PATH . $path;
|
if (is_file($destPath)) {
|
if (filesize($filePath) != filesize($destPath) || md5_file($filePath) != md5_file($destPath)) {
|
$list[] = $path;
|
}
|
}
|
} else {
|
$list[] = $path;
|
}
|
}
|
}
|
}
|
$list = array_filter(array_unique($list));
|
return $list;
|
}
|
|
/**
|
* 更新本地应用插件授权
|
*/
|
public static function authorization($params = [])
|
{
|
$addonList = get_addon_list();
|
$result = [];
|
$domain = request()->host(true);
|
$addons = [];
|
foreach ($addonList as $name => $item) {
|
$config = self::config($name);
|
$addons[] = ['name' => $name, 'domains' => $config['domains'] ?? [], 'licensecodes' => $config['licensecodes'] ?? [], 'validations' => $config['validations'] ?? []];
|
}
|
$params = array_merge($params, [
|
'faversion' => config('fastadmin.version'),
|
'domain' => $domain,
|
'addons' => $addons
|
]);
|
$result = self::sendRequest('/addon/authorization', $params, 'POST');
|
if (isset($result['code']) && $result['code'] == 1) {
|
$json = $result['data']['addons'] ?? [];
|
foreach ($addonList as $name => $item) {
|
self::config($name, ['domains' => $json[$name]['domains'] ?? [], 'licensecodes' => $json[$name]['licensecodes'] ?? [], 'validations' => $json[$name]['validations'] ?? []]);
|
}
|
return true;
|
} else {
|
throw new Exception($result['msg'] ?? __('Network error'));
|
}
|
}
|
|
/**
|
* 验证插件授权,应用插件需要授权使用,移除或绕过授权验证,保留追究法律责任的权利
|
* @param $name
|
* @return bool
|
*/
|
public static function checkAddonAuthorization($name)
|
{
|
$request = request();
|
$config = self::config($name);
|
$domain = self::getRootDomain($request->host(true));
|
//应用插件需要授权使用,移除或绕过授权验证,保留追究法律责任的权利
|
if (isset($config['domains']) && isset($config['domains']) && isset($config['validations']) && isset($config['licensecodes'])) {
|
$index = array_search($domain, $config['domains']);
|
if ((in_array($domain, $config['domains']) && in_array(md5(md5($domain) . ($config['licensecodes'][$index] ?? '')), $config['validations'])) || $request->isCli()) {
|
$request->bind('authorized', $domain ?: 'cli');
|
return true;
|
} elseif ($config['domains']) {
|
foreach ($config['domains'] as $index => $item) {
|
if (substr_compare($domain, "." . $item, -strlen("." . $item)) === 0 && in_array(md5(md5($item) . ($config['licensecodes'][$index] ?? '')), $config['validations'])) {
|
$request->bind('authorized', $domain);
|
return true;
|
}
|
}
|
}
|
}
|
return false;
|
}
|
|
/**
|
* 获取顶级域名
|
* @param $domain
|
* @return string
|
*/
|
public static function getRootDomain($domain)
|
{
|
$host = strtolower(trim($domain));
|
$hostArr = explode('.', $host);
|
$hostCount = count($hostArr);
|
$cnRegex = '/\w+\.(gov|org|ac|mil|net|edu|com|bj|tj|sh|cq|he|sx|nm|ln|jl|hl|js|zj|ah|fj|jx|sd|ha|hb|hn|gd|gx|hi|sc|gz|yn|xz|sn|gs|qh|nx|xj|tw|hk|mo)\.cn$/i';
|
$countryRegex = '/\w+\.(\w{2}|com|net)\.\w{2}$/i';
|
if ($hostCount > 2 && (preg_match($cnRegex, $host) || preg_match($countryRegex, $host))) {
|
$host = implode('.', array_slice($hostArr, -3, 3, true));
|
} else {
|
$host = implode('.', array_slice($hostArr, -2, 2, true));
|
}
|
return $host;
|
}
|
|
/**
|
* 获取插件行为、路由配置文件
|
* @return string
|
*/
|
public static function getExtraAddonsFile()
|
{
|
return CONF_PATH . 'extra' . DS . 'addons.php';
|
}
|
|
/**
|
* 获取bootstrap.js路径
|
* @return string
|
*/
|
public static function getBootstrapFile($name)
|
{
|
return ADDON_PATH . $name . DS . 'bootstrap.js';
|
}
|
|
/**
|
* 获取testdata.sql路径
|
* @return string
|
*/
|
public static function getTestdataFile($name)
|
{
|
return ADDON_PATH . $name . DS . 'testdata.sql';
|
}
|
|
/**
|
* 获取指定插件的目录
|
*/
|
public static function getAddonDir($name)
|
{
|
$dir = ADDON_PATH . $name . DS;
|
return $dir;
|
}
|
|
/**
|
* 获取插件备份目录
|
*/
|
public static function getAddonsBackupDir()
|
{
|
$dir = RUNTIME_PATH . 'addons' . DS;
|
if (!is_dir($dir)) {
|
@mkdir($dir, 0755, true);
|
}
|
return $dir;
|
}
|
|
/**
|
* 获取插件源资源文件夹
|
* @param string $name 插件名称
|
* @return string
|
*/
|
protected static function getSourceAssetsDir($name)
|
{
|
return ADDON_PATH . $name . DS . 'assets' . DS;
|
}
|
|
/**
|
* 获取插件目标资源文件夹
|
* @param string $name 插件名称
|
* @return string
|
*/
|
protected static function getDestAssetsDir($name)
|
{
|
$assetsDir = ROOT_PATH . str_replace("/", DS, "public/assets/addons/{$name}/");
|
return $assetsDir;
|
}
|
|
/**
|
* 获取远程服务器
|
* @return string
|
*/
|
protected static function getServerUrl()
|
{
|
return config('fastadmin.api_url');
|
}
|
|
/**
|
* 获取检测的全局文件夹目录
|
* @return array
|
*/
|
protected static function getCheckDirs()
|
{
|
return [
|
'application',
|
'public'
|
];
|
}
|
|
/**
|
* 获取请求对象
|
* @return Client
|
*/
|
public static function getClient()
|
{
|
$options = [
|
'base_uri' => self::getServerUrl(),
|
'timeout' => 30,
|
'connect_timeout' => 30,
|
'verify' => false,
|
'http_errors' => false,
|
'headers' => [
|
'X-REQUESTED-WITH' => 'XMLHttpRequest',
|
'Referer' => dirname(request()->root(true)),
|
'User-Agent' => 'FastAddon',
|
]
|
];
|
static $client;
|
if (empty($client)) {
|
$client = new Client($options);
|
}
|
return $client;
|
}
|
|
/**
|
* 发送请求
|
* @return array
|
* @throws Exception
|
* @throws \GuzzleHttp\Exception\GuzzleException
|
*/
|
public static function sendRequest($url, $params = [], $method = 'POST')
|
{
|
$json = [];
|
try {
|
$client = self::getClient();
|
$options = strtoupper($method) == 'POST' ? ['form_params' => $params] : ['query' => $params];
|
$response = $client->request($method, $url, $options);
|
$body = $response->getBody();
|
$content = $body->getContents();
|
$json = (array)json_decode($content, true);
|
} catch (TransferException $e) {
|
throw new Exception(__('Network error'));
|
} catch (\Exception $e) {
|
throw new Exception(__('Unknown data format'));
|
}
|
return $json;
|
}
|
|
/**
|
* 匹配配置文件中info信息
|
* @param ZipFile $zip
|
* @return array|false
|
* @throws Exception
|
*/
|
protected static function getInfoIni($zip)
|
{
|
$config = [];
|
// 读取插件信息
|
try {
|
$info = $zip->getEntryContents('info.ini');
|
$config = parse_ini_string($info);
|
} catch (ZipException $e) {
|
throw new Exception('Unable to extract the file');
|
}
|
return $config;
|
}
|
|
}
|