feat: PHP 网站增加禁用函数设置 (#1125)

Refs https://github.com/1Panel-dev/1Panel/issues/663
This commit is contained in:
zhengkunwang223 2023-05-24 16:31:18 +08:00 committed by GitHub
parent d7c08295f8
commit f77972fa38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 249 additions and 27 deletions

View File

@ -138,8 +138,10 @@ type WebsiteDefaultUpdate struct {
}
type WebsitePHPConfigUpdate struct {
ID uint `json:"id" validate:"required"`
Params map[string]string `json:"params" validate:"required"`
ID uint `json:"id" validate:"required"`
Params map[string]string `json:"params"`
Scope string `json:"scope" validate:"required"`
DisableFunctions []string `json:"disableFunctions"`
}
type WebsitePHPFileUpdate struct {

View File

@ -45,7 +45,8 @@ type WebsiteLog struct {
}
type PHPConfig struct {
Params map[string]string `json:"params"`
Params map[string]string `json:"params"`
DisableFunctions []string `json:"disableFunctions"`
}
type NginxRewriteRes struct {

View File

@ -16,6 +16,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/nginx/parser"
"github.com/1Panel-dev/1Panel/cmd/server/nginx_conf"
"golang.org/x/crypto/bcrypt"
"gopkg.in/ini.v1"
"gorm.io/gorm"
"os"
"path"
@ -1009,7 +1010,23 @@ func (w WebsiteService) GetPHPConfig(id uint) (*response.PHPConfig, error) {
params[matches[1]] = matches[2]
}
}
return &response.PHPConfig{Params: params}, nil
cfg, err := ini.Load(phpConfigPath)
if err != nil {
return nil, err
}
phpConfig, err := cfg.GetSection("PHP")
if err != nil {
return nil, err
}
disableFunctionStr := phpConfig.Key("disable_functions").Value()
res := &response.PHPConfig{Params: params}
if disableFunctionStr != "" {
disableFunctions := strings.Split(disableFunctionStr, ",")
if len(disableFunctions) > 0 {
res.DisableFunctions = disableFunctions
}
}
return res, nil
}
func (w WebsiteService) UpdatePHPConfig(req request.WebsitePHPConfigUpdate) (err error) {
@ -1033,23 +1050,50 @@ func (w WebsiteService) UpdatePHPConfig(req request.WebsitePHPConfigUpdate) (err
defer configFile.Close()
contentBytes, err := fileOp.GetContent(phpConfigPath)
content := string(contentBytes)
lines := strings.Split(content, "\n")
for i, line := range lines {
if strings.HasPrefix(line, ";") {
continue
}
for key, value := range req.Params {
pattern := "^" + regexp.QuoteMeta(key) + "\\s*=\\s*.*$"
if matched, _ := regexp.MatchString(pattern, line); matched {
lines[i] = key + " = " + value
}
}
}
updatedContent := strings.Join(lines, "\n")
if err := fileOp.WriteFile(phpConfigPath, strings.NewReader(updatedContent), 0755); err != nil {
if err != nil {
return err
}
if req.Scope == "params" {
content := string(contentBytes)
lines := strings.Split(content, "\n")
for i, line := range lines {
if strings.HasPrefix(line, ";") {
continue
}
for key, value := range req.Params {
pattern := "^" + regexp.QuoteMeta(key) + "\\s*=\\s*.*$"
if matched, _ := regexp.MatchString(pattern, line); matched {
lines[i] = key + " = " + value
}
}
}
updatedContent := strings.Join(lines, "\n")
if err := fileOp.WriteFile(phpConfigPath, strings.NewReader(updatedContent), 0755); err != nil {
return err
}
}
cfg, err := ini.Load(phpConfigPath)
if err != nil {
return err
}
phpConfig, err := cfg.GetSection("PHP")
if err != nil {
return err
}
if req.Scope == "disable_functions" {
disable := phpConfig.Key("disable_functions")
disable.SetValue(strings.Join(req.DisableFunctions, ","))
if err = cfg.SaveTo(phpConfigPath); err != nil {
return err
}
}
if req.Scope == "uploadSize" {
postMaxSize := phpConfig.Key("post_max_size")
postMaxSize.SetValue("")
}
appInstallReq := request.AppInstalledOperate{
InstallId: appInstall.ID,
Operate: constant.Restart,
@ -1058,6 +1102,7 @@ func (w WebsiteService) UpdatePHPConfig(req request.WebsitePHPConfigUpdate) (err
_ = fileOp.WriteFile(phpConfigPath, strings.NewReader(string(contentBytes)), 0755)
return err
}
return nil
}

View File

@ -44,7 +44,7 @@ declare module 'vue' {
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage'];
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber'];
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain'];
ElMenu: typeof import('element-plus/es')['ElMenu']
@ -76,7 +76,7 @@ declare module 'vue' {
FileRole: typeof import('./src/components/file-role/index.vue')['default']
FormButton: typeof import('./src/components/layout-content/form-button.vue')['default']
Group: typeof import('./src/components/group/index.vue')['default']
InfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll'];
InfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll']
LayoutContent: typeof import('./src/components/layout-content/index.vue')['default']
Line: typeof import('./src/components/v-charts/components/Line.vue')['default']
Loading: typeof import('element-plus/es')['ElLoadingDirective']

View File

@ -269,11 +269,14 @@ export namespace Website {
export interface PHPConfig {
params: any;
disableFunctions: string[];
}
export interface PHPConfigUpdate {
id: number;
params: any;
params?: any;
disableFunctions?: string[];
scope: string;
}
export interface PHPUpdate {

View File

@ -277,6 +277,19 @@ const checkConatinerName = (rule: any, value: any, callback: any) => {
}
};
const checkDisableFunctions = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.disableFunction')));
} else {
const reg = /^[a-zA-Z,]+$/;
if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.disableFunction')));
} else {
callback();
}
}
};
interface CommonRule {
requiredInput: FormItemRule;
requiredSelect: FormItemRule;
@ -300,6 +313,7 @@ interface CommonRule {
nginxDoc: FormItemRule;
appName: FormItemRule;
containerName: FormItemRule;
disabledFunctions: FormItemRule;
paramCommon: FormItemRule;
paramComplexity: FormItemRule;
@ -446,4 +460,9 @@ export const Rules: CommonRule = {
trigger: 'blur',
validator: checkConatinerName,
},
disabledFunctions: {
required: true,
trigger: 'blur',
validator: checkDisableFunctions,
},
};

View File

@ -159,6 +159,7 @@ const message = {
nginxDoc: 'Only supports English case, numbers, and .',
appName: 'Support English, numbers, - and _, length 2-30, and cannot start and end with -_',
conatinerName: 'Supports letters, numbers, underscores, hyphens and dots, cannot end with hyphen- or dot.',
disableFunction: 'Only support letters and,',
},
res: {
paramError: 'The request failed, please try again later!',
@ -1384,6 +1385,8 @@ const message = {
cgi_fix_pathinfo: 'Whether to open pathinfo',
date_timezone: 'Time zone',
second: 'Second',
disableFunction: 'Disable function',
disableFunctionHelper: 'Enter the function to be disabled, such as exec, please use multiple, split',
},
nginx: {
serverNamesHashBucketSizeHelper: 'The hash table size of the server name',

View File

@ -162,6 +162,7 @@ const message = {
nginxDoc: '仅支持英文大小写数字.',
appName: '支持英文数字-和_,长度2-30,并且不能以-_开头和结尾',
conatinerName: '支持字母数字下划线连字符和点,不能以连字符-或点.结尾',
disableFunction: '仅支持字母和,',
},
res: {
paramError: '请求失败,请稍后重试!',
@ -1365,6 +1366,8 @@ const message = {
cgi_fix_pathinfo: '是否开启pathinfo',
date_timezone: '时区',
second: '秒',
disableFunction: '禁用函数',
disableFunctionHelper: '输入要禁用的函数例如exec多个请用,分割',
},
nginx: {
serverNamesHashBucketSizeHelper: '服务器名字的hash表大小',

View File

@ -184,7 +184,7 @@ const submit = async () => {
display_errors: form.display_errors,
};
loading.value = true;
UpdatePHPConfig({ id: id.value, params: params })
UpdatePHPConfig({ id: id.value, params: params, scope: 'params' })
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
})

View File

@ -0,0 +1,142 @@
<template>
<div>
<el-row>
<el-col :xs="20" :sm="12" :md="10" :lg="10" :xl="8" :offset="1">
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item prop="funcs">
<el-input
type="text"
v-model="form.funcs"
label="value"
:placeholder="$t('php.disableFunctionHelper')"
/>
</el-form-item>
</el-form>
<ComplexTable :data="data" v-loading="loading">
<template #toolbar>
<el-button type="primary" icon="Plus" @click="openCreate(formRef)">
{{ $t('commons.button.add') }}
</el-button>
</template>
<el-table-column :label="$t('commons.table.name')" prop="func"></el-table-column>
<el-table-column :label="$t('commons.table.operate')">
<template #default="{ $index }">
<el-button link type="primary" @click="remove($index)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
</el-table-column>
</ComplexTable>
</el-col>
</el-row>
<ConfirmDialog ref="confirmDialogRef" @confirm="submit(false, [''])"></ConfirmDialog>
</div>
</template>
<script setup lang="ts">
import { GetPHPConfig, UpdatePHPConfig } from '@/api/modules/website';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { onMounted, reactive } from 'vue';
import { computed, ref } from 'vue';
const props = defineProps({
id: {
type: Number,
default: 0,
},
});
const rules = reactive({
funcs: [Rules.requiredInput, Rules.disabledFunctions],
});
const websiteID = computed(() => {
return props.id;
});
const formRef = ref();
const loading = ref(false);
const form = ref({
funcs: '',
});
const data = ref([]);
const confirmDialogRef = ref();
const search = () => {
loading.value = true;
GetPHPConfig(websiteID.value)
.then((res) => {
const functions = res.data.disableFunctions || [];
if (functions.length > 0) {
functions.forEach((value: string) => {
data.value.push({
func: value,
});
});
}
})
.finally(() => {
loading.value = false;
});
};
const openCreate = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialogRef.value!.acceptParams(params);
});
};
const remove = async (index: number) => {
ElMessageBox.confirm(i18n.global.t('commons.msg.delete'), i18n.global.t('commons.msg.deleteTitle'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
})
.then(() => {
const copyList = data.value.concat();
copyList.splice(index, 1);
const funcArray: string[] = [];
copyList.forEach((d) => {
funcArray.push(d.func);
});
submit(true, funcArray);
})
.catch(() => {});
};
const submit = async (del: boolean, funcArray: string[]) => {
let disableFunctions = [];
if (del) {
disableFunctions = funcArray;
} else {
disableFunctions = form.value.funcs.split(',');
data.value.forEach((d) => {
disableFunctions.push(d.func);
});
}
loading.value = true;
UpdatePHPConfig({ scope: 'disable_functions', id: websiteID.value, disableFunctions: disableFunctions })
.then(() => {
form.value.funcs = '';
data.value = [];
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
search();
})
.finally(() => {
loading.value = false;
});
};
onMounted(() => {
search();
});
</script>

View File

@ -3,6 +3,9 @@
<el-tab-pane :label="$t('website.updateConfig')" name="0">
<Config :id="id"></Config>
</el-tab-pane>
<el-tab-pane :label="$t('php.disableFunction')" name="1">
<Function :id="id"></Function>
</el-tab-pane>
</el-tabs>
</template>
@ -11,6 +14,7 @@ import { GetRuntime } from '@/api/modules/runtime';
import { GetWebsite } from '@/api/modules/website';
import { computed, onMounted, ref } from 'vue';
import Config from './config/index.vue';
import Function from './function/index.vue';
const props = defineProps({
id: {
@ -23,9 +27,9 @@ const id = computed(() => {
return props.id;
});
let index = ref('0');
let configPHP = ref(false);
let installId = ref(0);
const index = ref('0');
const configPHP = ref(false);
const installId = ref(0);
const getWebsiteDetail = async () => {
const res = await GetWebsite(props.id);

View File

@ -153,7 +153,6 @@
</el-card>
</template>
</LayoutContent>
<NginxConfig v-if="openNginxConfig" v-loading="loading" :containerName="containerName" :status="nginxStatus" />
<CreateWebSite ref="createRef" @close="search" />
<DeleteWebsite ref="deleteRef" @close="search" />
<UploadDialog ref="uploadRef" />
@ -161,6 +160,7 @@
<DefaultServer ref="defaultRef" />
<GroupDialog @search="listGroup" ref="groupRef" />
</div>
<NginxConfig v-if="openNginxConfig" v-loading="loading" :containerName="containerName" :status="nginxStatus" />
</template>
<script lang="ts" setup>