// Copyright 2023 The Gitea Authors. All rights reserved. // Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package main import ( "go/token" "os" "text/template" tmplParser "text/template/parse" fjTemplates "forgejo.org/modules/templates" "forgejo.org/modules/util" ) // derived from source: modules/templates/scopedtmpl/scopedtmpl.go, L169-L213 func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) { switch node.Type() { case tmplParser.NodeAction: handler.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe) case tmplParser.NodeList: nodeList := node.(*tmplParser.ListNode) handler.handleTemplateFileNodes(fset, nodeList.Nodes) case tmplParser.NodePipe: handler.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode)) case tmplParser.NodeTemplate: handler.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe) case tmplParser.NodeIf: nodeIf := node.(*tmplParser.IfNode) handler.handleTemplateBranchNode(fset, nodeIf.BranchNode) case tmplParser.NodeRange: nodeRange := node.(*tmplParser.RangeNode) handler.handleTemplateBranchNode(fset, nodeRange.BranchNode) case tmplParser.NodeWith: nodeWith := node.(*tmplParser.WithNode) handler.handleTemplateBranchNode(fset, nodeWith.BranchNode) case tmplParser.NodeCommand: nodeCommand := node.(*tmplParser.CommandNode) handler.handleTemplateFileNodes(fset, nodeCommand.Args) if len(nodeCommand.Args) < 2 { return } funcname := "" if nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode); ok { if nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode); ok { if nodeIdent.Ident != "ctx" || len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" { return } funcname = nodeChain.Field[1] } } else if nodeField, ok := nodeCommand.Args[0].(*tmplParser.FieldNode); ok { if len(nodeField.Ident) != 2 || !(nodeField.Ident[0] == "locale" || nodeField.Ident[0] == "Locale") { return } funcname = nodeField.Ident[1] } var gotUnexpectedInvoke *int ltf, ok := handler.LocaleTrFunctions[funcname] if !ok { return } for _, argNum := range ltf { if len(nodeCommand.Args) >= int(argNum+2) { nodeString, ok := nodeCommand.Args[int(argNum+1)].(*tmplParser.StringNode) if ok { // found interesting strings // the column numbers are a bit "off", but much better than nothing handler.OnMsgid(fset, token.Pos(nodeString.Pos), nodeString.Text) } } else { argc := len(nodeCommand.Args) - 1 gotUnexpectedInvoke = &argc } } if gotUnexpectedInvoke != nil { handler.OnUnexpectedInvoke(fset, token.Pos(nodeCommand.Pos), funcname, *gotUnexpectedInvoke) } default: } } func (handler Handler) handleTemplatePipeNode(fset *token.FileSet, pipeNode *tmplParser.PipeNode) { if pipeNode == nil { return } // NOTE: we can't pass `pipeNode.Cmds` to handleTemplateFileNodes due to incompatible argument types for _, node := range pipeNode.Cmds { handler.handleTemplateNode(fset, node) } } func (handler Handler) handleTemplateBranchNode(fset *token.FileSet, branchNode tmplParser.BranchNode) { handler.handleTemplatePipeNode(fset, branchNode.Pipe) handler.handleTemplateFileNodes(fset, branchNode.List.Nodes) if branchNode.ElseList != nil { handler.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes) } } func (handler Handler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) { for _, node := range nodes { handler.handleTemplateNode(fset, node) } } // the `Handle*File` functions follow the following calling convention: // * `fname` is the name of the input file // * `src` is either `nil` (then the function invokes `ReadFile` to read the file) // or the contents of the file as {`[]byte`, or a `string`} func (handler Handler) HandleTemplateFile(fname string, src any) error { var tmplContent []byte switch src2 := src.(type) { case nil: var err error tmplContent, err = os.ReadFile(fname) if err != nil { return LocatedError{ Location: fname, Kind: "ReadFile", Err: err, } } case []byte: tmplContent = src2 case string: // SAFETY: we do not modify tmplContent below tmplContent = util.UnsafeStringToBytes(src2) default: panic("invalid type for 'src'") } fset := token.NewFileSet() fset.AddFile(fname, 1, len(tmplContent)).SetLinesForContent(tmplContent) // SAFETY: we do not modify tmplContent2 below tmplContent2 := util.UnsafeBytesToString(tmplContent) tmpl := template.New(fname) tmpl.Funcs(fjTemplates.NewFuncMap()) tmplParsed, err := tmpl.Parse(tmplContent2) if err != nil { return LocatedError{ Location: fname, Kind: "Template parser", Err: err, } } handler.handleTemplateFileNodes(fset, tmplParsed.Root.Nodes) return nil }