mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-09 00:21:31 +01:00
366 lines
10 KiB
Go
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)
|
|
}
|