import re from django import forms from django.contrib.auth.models import User from PIL import Image from . import versions as rdeskVersions # 应用名/公司名会被插入到单/双引号包裹的 bash sed 脚本中:& \ | 会导致替换 # 静默失败,' " $ ` 会破坏 shell 引号,CR/LF 会破坏 sed 寻址。 UNSAFE_NAME_CHARS = re.compile(r'[&\\|\'"$`\r\n]') UNSAFE_NAME_MESSAGE = "包含构建脚本不支持的字符(& \\ | ' \" $ ` 或换行符)。" # 统一的控件样式:文本/数字/下拉/文件输入框 .form-control,复选/单选 .form-check-input # 注意:Django 5.x 中 NumberInput 直接继承 Input(而非 TextInput),必须单独列出 _CONTROL_WIDGETS = ( forms.TextInput, forms.NumberInput, forms.PasswordInput, forms.EmailInput, forms.Select, forms.Textarea, forms.FileInput, ) def apply_control_classes(form): for field in form.fields.values(): widget = field.widget if isinstance(widget, _CONTROL_WIDGETS): widget.attrs.setdefault('class', 'form-control') elif isinstance(widget, forms.CheckboxInput): widget.attrs.setdefault('class', 'form-check-input') class GenerateForm(forms.Form): sh_secret_field = forms.CharField(required=False, widget=forms.HiddenInput) # 平台与版本 platform = forms.ChoiceField( label="目标平台", choices=[ ('windows', 'Windows 64 位'), ('windows-x86', 'Windows 32 位'), ('linux', 'Linux'), ('android', 'Android'), ('macos', 'macOS'), ], initial='windows', ) version = forms.ChoiceField( label="RustDesk 版本", choices=[('master', '开发版(nightly 每夜构建)')] + [(v, v) for v in rdeskVersions.FALLBACK_VERSIONS], initial='1.4.9', help_text=( "'master' 为开发版(每夜构建),包含最新特性,但稳定性可能不如正式版本。" "正式版列表自动取自 GitHub 官方仓库 rustdesk/rustdesk,跟随官方发版更新。" ), ) delayFix = forms.BooleanField(label="修复使用第三方 API 时的连接延迟问题", initial=True, required=False) # 常规设置 exename = forms.CharField( label="配置名称", required=True, help_text="仅限英文、数字与下划线,不含空格;将同时作为生成文件的名称。", ) appname = forms.CharField( label="自定义应用名称", required=False, help_text="留空则使用默认名称 rustdesk。", ) direction = forms.ChoiceField( label="连接方向", widget=forms.RadioSelect, choices=[ ('incoming', '仅受控(只能被他人远程连接)'), ('outgoing', '仅主控(只能主动连接他人)'), ('both', '主控 / 受控双向(默认)'), ], initial='both', ) installation = forms.ChoiceField( label="是否允许安装", choices=[ ('installationY', '允许安装(默认)'), ('installationN', '禁止安装(绿色单文件运行)'), ], initial='installationY', ) settings = forms.ChoiceField( label="是否允许修改设置", choices=[ ('settingsY', '允许修改设置(默认)'), ('settingsN', '禁止修改设置'), ], initial='settingsY', ) androidappid = forms.CharField( label="自定义 Android 应用 ID", required=False, help_text="替换默认 com.carriez.flutter_hbb,留空使用默认值。", ) # 自定义服务器 serverIP = forms.CharField( label="ID / 中继服务器地址", required=False, help_text="填写自建服务器的主机名或 IP。", ) serverPort = forms.CharField(label="服务器端口", required=False) apiServer = forms.CharField( label="API 服务器地址", required=False, help_text="留空时默认使用「服务器地址:21114」。", ) key = forms.CharField(label="公钥(Key)", required=False) urlLink = forms.CharField( label="自定义官网链接", required=False, help_text="替换默认的 https://rustdesk.com。", ) downloadLink = forms.CharField( label="更新下载链接", required=False, help_text="替换默认的 https://rustdesk.com/download。", ) compname = forms.CharField( label="版权公司名称", required=False, help_text="替换默认的 Purslane Tech Pte. Ltd.。", ) # 外观 iconfile = forms.FileField( label="应用图标", required=False, widget=forms.FileInput(attrs={'accept': 'image/png'}), help_text="PNG 格式,建议 256×256 正方形。", ) logofile = forms.FileField( label="应用 Logo", required=False, widget=forms.FileInput(attrs={'accept': 'image/png'}), help_text="PNG 格式。", ) privacyfile = forms.FileField( label="隐私屏图片", required=False, widget=forms.FileInput(attrs={'accept': 'image/png'}), help_text="PNG 格式。", ) iconbase64 = forms.CharField(required=False, widget=forms.HiddenInput) logobase64 = forms.CharField(required=False, widget=forms.HiddenInput) privacybase64 = forms.CharField(required=False, widget=forms.HiddenInput) theme = forms.ChoiceField( label="主题", choices=[('light', '浅色'), ('dark', '深色'), ('system', '跟随系统')], initial='system', ) themeDorO = forms.ChoiceField( label="主题生效方式", choices=[('default', '默认(允许客户端更改)'), ('override', '强制锁定(客户端无法更改)')], initial='default', ) # 安全 passApproveMode = forms.ChoiceField( label="连接确认方式", choices=[ ('password', '仅通过密码接受会话'), ('click', '仅通过点击接受会话'), ('password-click', '密码或点击均可(默认)'), ], initial='password-click', ) permanentPassword = forms.CharField( label="设置固定访问密码", widget=forms.PasswordInput(), required=False, help_text="该密码为客户端默认密码,用户仍可在客户端自行修改。", ) denyLan = forms.BooleanField(label="禁止局域网发现", initial=False, required=False) enableDirectIP = forms.BooleanField(label="允许直接 IP 直连", initial=False, required=False) autoClose = forms.BooleanField(label="用户无操作时自动断开入站会话", initial=False, required=False) hidecm = forms.BooleanField(label="允许在远程画面中隐藏连接窗口", initial=False, required=False) # 权限 permissionsDorO = forms.ChoiceField( label="权限生效方式", choices=[('default', '默认(用户可更改)'), ('override', '强制锁定(用户无法更改)')], initial='default', ) permissionsType = forms.ChoiceField( label="权限预设", choices=[('custom', '自定义'), ('full', '完全控制'), ('view', '仅查看(屏幕共享)')], initial='custom', ) enableKeyboard = forms.BooleanField(label="键盘 / 鼠标控制", initial=True, required=False) enableClipboard = forms.BooleanField(label="剪贴板同步", initial=True, required=False) enableFileTransfer = forms.BooleanField(label="文件传输", initial=True, required=False) enableAudio = forms.BooleanField(label="音频传输", initial=True, required=False) enableTCP = forms.BooleanField(label="TCP 隧道", initial=True, required=False) enableRemoteRestart = forms.BooleanField(label="远程重启", initial=True, required=False) enableRecording = forms.BooleanField(label="会话录制", initial=True, required=False) enableBlockingInput = forms.BooleanField(label="屏蔽被控端键鼠输入", initial=True, required=False) enableRemoteModi = forms.BooleanField(label="允许修改远程配置", initial=False, required=False) enablePrinter = forms.BooleanField(label="远程打印机", initial=True, required=False) enableCamera = forms.BooleanField(label="远程摄像头", initial=True, required=False) enableTerminal = forms.BooleanField(label="远程终端", initial=True, required=False) # 其他 removeWallpaper = forms.BooleanField(label="入站会话期间移除桌面壁纸", initial=True, required=False) defaultManual = forms.CharField( label="默认高级设置", widget=forms.Textarea(attrs={'rows': 4}), required=False, help_text="每行一项,格式:键=值。", ) overrideManual = forms.CharField( label="强制高级设置", widget=forms.Textarea(attrs={'rows': 4}), required=False, help_text="每行一项,格式:键=值;设置后客户端用户无法修改。", ) # 定制功能 xOffline = forms.BooleanField(label="通讯录中离线设备显示 X 标记", initial=False, required=False) removeNewVersionNotif = forms.BooleanField(label="移除新版本更新通知", initial=False, required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) apply_control_classes(self) def clean_version(self): # 视图会在绑定前把 choices 刷新为官方最新列表;这里再做宽松校验, # 兼容「打开页面后列表刷新 / 官方最新 tag 尚未进列表」等窗口期。 value = self.cleaned_data.get('version', '') if not rdeskVersions.is_valid_version(value): raise forms.ValidationError("版本号无效,请从列表中选择或填写形如 1.4.9 的正式版本号。") return value def clean_iconfile(self): image = self.cleaned_data['iconfile'] if image: try: img = Image.open(image) if img.format != 'PNG': raise forms.ValidationError("仅支持 PNG 格式的图标。") width, height = img.size if width != height: raise forms.ValidationError("应用图标必须为正方形图片。") return image except forms.ValidationError: raise except OSError: raise forms.ValidationError("图标文件无效或已损坏。") except Exception as e: raise forms.ValidationError(f"处理图标时出错:{e}") def _reject_unsafe_name_chars(self, field): value = self.cleaned_data.get(field, '') if value and UNSAFE_NAME_CHARS.search(value): raise forms.ValidationError(UNSAFE_NAME_MESSAGE) return value def clean_appname(self): return self._reject_unsafe_name_chars('appname') def clean_compname(self): return self._reject_unsafe_name_chars('compname') class ApiTokenForm(forms.Form): name = forms.CharField(label="令牌名称", max_length=100, required=True, help_text="建议按用途命名,例如:办公网构建机、CI 流水线。") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) apply_control_classes(self) class GitHubSettingsForm(forms.Form): """后台「构建平台配置」表单(GitHub / Gitea)。 非密钥项直接显示生效值;密钥项不回显明文,留空表示不修改, 勾选「清除」则删除后台覆盖、回退到环境变量/默认值。 """ # 构建平台 build_platform = forms.ChoiceField( label="构建平台", choices=[('github', 'GitHub Actions'), ('gitea', 'Gitea Actions(自建)')], widget=forms.RadioSelect, required=False, ) # Gitea 连接 gitea_server_url = forms.CharField( label="Gitea 服务器地址", max_length=255, required=False, widget=forms.TextInput(attrs={'placeholder': 'https://gitea.example.com'}), ) gitea_owner = forms.CharField(label="Gitea 仓库属主", max_length=100, required=False) gitea_repo = forms.CharField(label="Gitea 仓库名称", max_length=100, required=False) gitea_branch = forms.CharField(label="Gitea 工作流分支", max_length=100, required=False) gitea_token = forms.CharField( label="Gitea 访问令牌", widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), required=False, ) clear_gitea_token = forms.BooleanField(label="清除后台保存的令牌", required=False) gitea_web_username = forms.CharField( label="Gitea 网页登录账号(可选)", max_length=100, required=False, widget=forms.TextInput(attrs={'placeholder': '仅 Gitea 1.27 及更早版本一键停止时需要'}), ) gitea_web_password = forms.CharField( label="Gitea 网页登录密码(可选)", widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), required=False, ) clear_gitea_web_password = forms.BooleanField(label="清除后台保存的网页登录密码", required=False) gitea_proxy = forms.CharField( label="Gitea 访问代理", widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '内网直连通常留空,如 http://host:port'}), required=False, ) clear_gitea_proxy = forms.BooleanField(label="清除后台保存的代理", required=False) # GitHub 基础连接 ghuser = forms.CharField(label="GitHub 用户名", max_length=100, required=False) reponame = forms.CharField(label="仓库名称", max_length=100, required=False) ghbranch = forms.CharField(label="工作流分支", max_length=100, required=False) ghbearer = forms.CharField( label="GitHub 访问令牌", widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), required=False, ) clear_ghbearer = forms.BooleanField(label="清除后台保存的令牌", required=False) # 回调与加密 genurl = forms.CharField( label="本平台外部访问地址", max_length=255, required=False, widget=forms.TextInput(attrs={'placeholder': 'https://rdgen.example.com'}), ) protocol = forms.ChoiceField( label="平台访问协议", choices=[('https', 'https'), ('http', 'http')], required=False, ) zip_password = forms.CharField( label="配置压缩包密码", widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), required=False, ) clear_zip_password = forms.BooleanField(label="清除后台保存的密码", required=False) # 高级 sh_secret = forms.CharField( label="自托管 Runner 密钥", widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), required=False, ) clear_sh_secret = forms.BooleanField(label="清除后台保存的密钥", required=False) webhook_secret = forms.CharField( label="回调校验密钥", widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), required=False, ) clear_webhook_secret = forms.BooleanField(label="清除后台保存的密钥", required=False) # 网络代理 gh_proxy = forms.CharField( label="GitHub 访问代理", widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '留空直连,如 http://127.0.0.1:7890'}), required=False, ) clear_gh_proxy = forms.BooleanField(label="清除后台保存的代理", required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) apply_control_classes(self) class MaintenanceSettingsForm(forms.Form): """后台「维护设置」:构建任务定时清理与数据库定时备份。""" build_retention_days = forms.IntegerField( label="构建任务保留天数", min_value=0, max_value=3650, initial=30, help_text="超过该天数的已结束任务(成功 / 失败 / 取消)及安装包产物会被自动清理;0 表示永不自动清理。进行中的任务不会被清理。", ) db_backup_enabled = forms.BooleanField( label="启用数据库定时备份(每天自动备份一次)", required=False, initial=True, ) db_backup_keep = forms.IntegerField( label="数据库备份保留份数", min_value=1, max_value=100, initial=7, help_text="超出份数的最旧备份会被自动删除。备份文件保存在持久化数据卷中,可在下方列表下载。", ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) apply_control_classes(self) class AdminUserCreateForm(forms.Form): username = forms.CharField(label="用户名", max_length=150, required=True) email = forms.EmailField(label="邮箱", required=False) password = forms.CharField(label="登录密码", widget=forms.PasswordInput, required=True, min_length=8, help_text="至少 8 位。") password_confirm = forms.CharField(label="确认密码", widget=forms.PasswordInput, required=True) is_staff = forms.BooleanField(label="设为管理员(可访问后台管理、查看所有任务)", required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) apply_control_classes(self) def clean_username(self): username = self.cleaned_data['username'].strip() if User.objects.filter(username=username).exists(): raise forms.ValidationError("该用户名已存在。") return username def clean(self): cleaned = super().clean() if cleaned.get('password') and cleaned.get('password') != cleaned.get('password_confirm'): self.add_error('password_confirm', "两次输入的密码不一致。") return cleaned class AdminUserEditForm(forms.Form): email = forms.EmailField(label="邮箱", required=False) is_active = forms.BooleanField(label="账号启用", required=False) is_staff = forms.BooleanField(label="管理员权限(可访问后台管理)", required=False) new_password = forms.CharField( label="重置密码(留空表示不修改)", widget=forms.PasswordInput, required=False, min_length=8, ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) apply_control_classes(self)