0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-12-09 00:21:31 +01:00
gitea/routers/web/admin/render_plugins.go
2025-12-06 15:13:35 -08:00

366 lines
10 KiB
Go

// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strings"
"sync"
render_model "code.gitea.io/gitea/models/render"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
plugin_service "code.gitea.io/gitea/services/renderplugin"
)
const (
tplRenderPlugins templates.TplName = "admin/render/plugins"
tplRenderPluginDetail templates.TplName = "admin/render/plugin_detail"
tplRenderPluginConfirm templates.TplName = "admin/render/plugin_confirm"
)
type pendingRenderPluginUpload struct {
Path string
Filename string
ExpectedIdentifier string
PluginID int64
}
var (
pendingUploadsMu sync.Mutex
pendingUploads = make(map[string]*pendingRenderPluginUpload)
)
func rememberPendingUpload(info *pendingRenderPluginUpload) (string, error) {
for {
token, err := util.CryptoRandomString(32)
if err != nil {
return "", err
}
pendingUploadsMu.Lock()
if _, ok := pendingUploads[token]; ok {
pendingUploadsMu.Unlock()
continue
}
pendingUploads[token] = info
pendingUploadsMu.Unlock()
return token, nil
}
}
func takePendingUpload(token string) *pendingRenderPluginUpload {
if token == "" {
return nil
}
pendingUploadsMu.Lock()
defer pendingUploadsMu.Unlock()
info := pendingUploads[token]
delete(pendingUploads, token)
return info
}
func discardPendingUpload(info *pendingRenderPluginUpload) {
if info == nil {
return
}
if err := os.Remove(info.Path); err != nil && !os.IsNotExist(err) {
log.Warn("Failed to remove pending render plugin upload %s: %v", info.Path, err)
}
}
// RenderPlugins shows the plugin management page.
func RenderPlugins(ctx *context.Context) {
plugs, err := render_model.ListPlugins(ctx)
if err != nil {
ctx.ServerError("ListPlugins", err)
return
}
ctx.Data["Title"] = ctx.Tr("admin.render_plugins")
ctx.Data["PageIsAdminRenderPlugins"] = true
ctx.Data["Plugins"] = plugs
ctx.HTML(http.StatusOK, tplRenderPlugins)
}
// RenderPluginDetail shows a single plugin detail page.
func RenderPluginDetail(ctx *context.Context) {
plug := mustGetRenderPlugin(ctx)
if plug == nil {
return
}
ctx.Data["Title"] = ctx.Tr("admin.render_plugins.detail_title", plug.Name)
ctx.Data["PageIsAdminRenderPlugins"] = true
ctx.Data["Plugin"] = plug
ctx.HTML(http.StatusOK, tplRenderPluginDetail)
}
// RenderPluginsUpload handles plugin uploads.
func RenderPluginsUpload(ctx *context.Context) {
file, header, err := ctx.Req.FormFile("plugin")
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upload_failed", err))
redirectRenderPlugins(ctx)
return
}
defer file.Close()
if header.Size == 0 {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upload_missing"))
redirectRenderPlugins(ctx)
return
}
previewPath, err := saveRenderPluginUpload(file)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upload_failed", err))
redirectRenderPlugins(ctx)
return
}
manifest, err := plugin_service.LoadManifestFromArchive(previewPath)
if err != nil {
_ = os.Remove(previewPath)
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upload_failed", err))
redirectRenderPlugins(ctx)
return
}
token, err := rememberPendingUpload(&pendingRenderPluginUpload{
Path: previewPath,
Filename: header.Filename,
ExpectedIdentifier: "",
PluginID: 0,
})
if err != nil {
_ = os.Remove(previewPath)
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upload_failed", err))
redirectRenderPlugins(ctx)
return
}
ctx.Data["Title"] = ctx.Tr("admin.render_plugins.confirm_install", manifest.Name)
ctx.Data["PageIsAdminRenderPlugins"] = true
ctx.Data["PluginManifest"] = manifest
ctx.Data["UploadFilename"] = header.Filename
ctx.Data["PendingUploadToken"] = token
ctx.Data["IsUpgradePreview"] = false
ctx.Data["RedirectTo"] = ctx.FormString("redirect_to")
ctx.HTML(http.StatusOK, tplRenderPluginConfirm)
}
// RenderPluginsEnable toggles plugin state to enabled.
func RenderPluginsEnable(ctx *context.Context) {
plug := mustGetRenderPlugin(ctx)
if plug == nil {
return
}
if err := plugin_service.SetEnabled(ctx, plug, true); err != nil {
ctx.Flash.Error(err.Error())
} else {
ctx.Flash.Success(ctx.Tr("admin.render_plugins.enabled", plug.Name))
}
redirectRenderPlugins(ctx)
}
// RenderPluginsDisable toggles plugin state to disabled.
func RenderPluginsDisable(ctx *context.Context) {
plug := mustGetRenderPlugin(ctx)
if plug == nil {
return
}
if err := plugin_service.SetEnabled(ctx, plug, false); err != nil {
ctx.Flash.Error(err.Error())
} else {
ctx.Flash.Success(ctx.Tr("admin.render_plugins.disabled", plug.Name))
}
redirectRenderPlugins(ctx)
}
// RenderPluginsDelete removes a plugin entirely.
func RenderPluginsDelete(ctx *context.Context) {
plug := mustGetRenderPlugin(ctx)
if plug == nil {
return
}
if err := plugin_service.Delete(ctx, plug); err != nil {
ctx.Flash.Error(err.Error())
} else {
ctx.Flash.Success(ctx.Tr("admin.render_plugins.deleted", plug.Name))
}
redirectRenderPlugins(ctx)
}
// RenderPluginsUpgrade upgrades an existing plugin with a new archive.
func RenderPluginsUpgrade(ctx *context.Context) {
plug := mustGetRenderPlugin(ctx)
if plug == nil {
return
}
file, header, err := ctx.Req.FormFile("plugin")
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upgrade_failed", err))
redirectRenderPlugins(ctx)
return
}
defer file.Close()
if header.Size == 0 {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upload_missing"))
redirectRenderPlugins(ctx)
return
}
previewPath, err := saveRenderPluginUpload(file)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upgrade_failed", err))
redirectRenderPlugins(ctx)
return
}
manifest, err := plugin_service.LoadManifestFromArchive(previewPath)
if err != nil {
_ = os.Remove(previewPath)
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upgrade_failed", err))
redirectRenderPlugins(ctx)
return
}
if manifest.ID != plug.Identifier {
_ = os.Remove(previewPath)
ctx.Flash.Error(ctx.Tr("admin.render_plugins.identifier_mismatch", manifest.ID, plug.Identifier))
redirectRenderPlugins(ctx)
return
}
token, err := rememberPendingUpload(&pendingRenderPluginUpload{
Path: previewPath,
Filename: header.Filename,
ExpectedIdentifier: plug.Identifier,
PluginID: plug.ID,
})
if err != nil {
_ = os.Remove(previewPath)
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upgrade_failed", err))
redirectRenderPlugins(ctx)
return
}
ctx.Data["Title"] = ctx.Tr("admin.render_plugins.confirm_upgrade", plug.Name)
ctx.Data["PageIsAdminRenderPlugins"] = true
ctx.Data["PluginManifest"] = manifest
ctx.Data["UploadFilename"] = header.Filename
ctx.Data["PendingUploadToken"] = token
ctx.Data["IsUpgradePreview"] = true
ctx.Data["CurrentPlugin"] = plug
ctx.Data["RedirectTo"] = ctx.FormString("redirect_to")
ctx.HTML(http.StatusOK, tplRenderPluginConfirm)
}
// RenderPluginsUploadConfirm finalizes a pending plugin installation.
func RenderPluginsUploadConfirm(ctx *context.Context) {
info := takePendingUpload(ctx.FormString("token"))
if info == nil || info.PluginID != 0 {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upload_token_invalid"))
if info != nil {
discardPendingUpload(info)
}
redirectRenderPlugins(ctx)
return
}
_, err := installPendingUpload(ctx, info)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upload_failed", err))
} else {
ctx.Flash.Success(ctx.Tr("admin.render_plugins.upload_success", info.Filename))
}
redirectRenderPlugins(ctx)
}
// RenderPluginsUpgradeConfirm finalizes a pending plugin upgrade.
func RenderPluginsUpgradeConfirm(ctx *context.Context) {
plug := mustGetRenderPlugin(ctx)
if plug == nil {
return
}
info := takePendingUpload(ctx.FormString("token"))
if info == nil || info.PluginID != plug.ID || info.ExpectedIdentifier != plug.Identifier {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upload_token_invalid"))
if info != nil {
discardPendingUpload(info)
}
redirectRenderPlugins(ctx)
return
}
updated, err := installPendingUpload(ctx, info)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.upgrade_failed", err))
} else {
ctx.Flash.Success(ctx.Tr("admin.render_plugins.upgrade_success", updated.Name, updated.Version))
}
redirectRenderPlugins(ctx)
}
// RenderPluginsUploadDiscard removes a pending upload archive without installing it.
func RenderPluginsUploadDiscard(ctx *context.Context) {
info := takePendingUpload(ctx.FormString("token"))
if info != nil {
discardPendingUpload(info)
}
ctx.Flash.Success(ctx.Tr("admin.render_plugins.upload_discarded"))
redirectRenderPlugins(ctx)
}
func mustGetRenderPlugin(ctx *context.Context) *render_model.Plugin {
id := ctx.PathParamInt64("id")
if id <= 0 {
ctx.Flash.Error(ctx.Tr("admin.render_plugins.invalid"))
redirectRenderPlugins(ctx)
return nil
}
plug, err := render_model.GetPluginByID(ctx, id)
if err != nil {
ctx.Flash.Error(fmt.Sprintf("%v", err))
redirectRenderPlugins(ctx)
return nil
}
return plug
}
func redirectRenderPlugins(ctx *context.Context) {
redirectTo := ctx.FormString("redirect_to")
if redirectTo != "" {
base := setting.AppSubURL + "/"
if strings.HasPrefix(redirectTo, base) {
ctx.Redirect(redirectTo)
return
}
}
ctx.Redirect(setting.AppSubURL + "/-/admin/render-plugins")
}
func saveRenderPluginUpload(file multipart.File) (_ string, err error) {
tmpFile, cleanup, err := setting.AppDataTempDir("render-plugins").CreateTempFileRandom("pending", "*.zip")
if err != nil {
return "", err
}
defer func() {
if err != nil {
cleanup()
}
}()
if _, err = io.Copy(tmpFile, file); err != nil {
return "", err
}
if err = tmpFile.Close(); err != nil {
return "", err
}
return tmpFile.Name(), nil
}
func installPendingUpload(ctx *context.Context, info *pendingRenderPluginUpload) (*render_model.Plugin, error) {
file, err := os.Open(info.Path)
if err != nil {
discardPendingUpload(info)
return nil, err
}
defer file.Close()
defer discardPendingUpload(info)
return plugin_service.InstallFromArchive(ctx, file, info.Filename, info.ExpectedIdentifier)
}