github.com/mattermost/mattermost-server/server/v8@v8.0.0-20230610055354-a6d1d38b273d/platform/shared/templates/templates.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package templates
     5  
     6  import (
     7  	"bytes"
     8  	"html/template"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"sync"
    13  
    14  	"github.com/fsnotify/fsnotify"
    15  
    16  	"github.com/mattermost/mattermost-server/server/v8/channels/utils/fileutils"
    17  )
    18  
    19  // Container represents a set of templates that can be render
    20  type Container struct {
    21  	templates *template.Template
    22  	mutex     sync.RWMutex
    23  	stop      chan struct{}
    24  	stopped   chan struct{}
    25  	watch     bool
    26  }
    27  
    28  // Data contains the data used to populate the template variables, it has Props
    29  // that can be of any type and HTML that only can be `template.HTML` types.
    30  type Data struct {
    31  	Props map[string]any
    32  	HTML  map[string]template.HTML
    33  }
    34  
    35  func GetTemplateDirectory() (string, bool) {
    36  	templatesDir := "templates"
    37  	if mattermostPath := os.Getenv("MM_SERVER_PATH"); mattermostPath != "" {
    38  		templatesDir = filepath.Join(mattermostPath, templatesDir)
    39  	}
    40  
    41  	return fileutils.FindDir(templatesDir)
    42  }
    43  
    44  // NewFromTemplates creates a new templates container using a
    45  // `template.Template` object
    46  func NewFromTemplate(templates *template.Template) *Container {
    47  	return &Container{templates: templates}
    48  }
    49  
    50  // New creates a new templates container scanning a directory.
    51  func New(directory string) (*Container, error) {
    52  	c := &Container{}
    53  
    54  	htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html"))
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	c.templates = htmlTemplates
    59  
    60  	return c, nil
    61  }
    62  
    63  // NewWithWatcher creates a new templates container scanning a directory and
    64  // watch the directory filesystem changes to apply them to the loaded
    65  // templates. This function returns the container and an errors channel to pass
    66  // all errors that can happen during the watch process, or an regular error if
    67  // we fail to create the templates or the watcher. The caller must consume the
    68  // returned errors channel to ensure not blocking the watch process.
    69  func NewWithWatcher(directory string) (*Container, <-chan error, error) {
    70  	htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html"))
    71  	if err != nil {
    72  		return nil, nil, err
    73  	}
    74  
    75  	watcher, err := fsnotify.NewWatcher()
    76  	if err != nil {
    77  		return nil, nil, err
    78  	}
    79  
    80  	err = watcher.Add(directory)
    81  	if err != nil {
    82  		watcher.Close()
    83  		return nil, nil, err
    84  	}
    85  
    86  	c := &Container{
    87  		templates: htmlTemplates,
    88  		watch:     true,
    89  		stop:      make(chan struct{}),
    90  		stopped:   make(chan struct{}),
    91  	}
    92  	errors := make(chan error)
    93  
    94  	go func() {
    95  		defer close(errors)
    96  		defer close(c.stopped)
    97  		defer watcher.Close()
    98  
    99  		for {
   100  			select {
   101  			case <-c.stop:
   102  				return
   103  			case event := <-watcher.Events:
   104  				if event.Op&fsnotify.Write == fsnotify.Write {
   105  					if htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html")); err != nil {
   106  						errors <- err
   107  					} else {
   108  						c.mutex.Lock()
   109  						c.templates = htmlTemplates
   110  						c.mutex.Unlock()
   111  					}
   112  				}
   113  			case err := <-watcher.Errors:
   114  				errors <- err
   115  			}
   116  		}
   117  	}()
   118  
   119  	return c, errors, nil
   120  }
   121  
   122  // Close stops the templates watcher of the container in case you have created
   123  // it with watch parameter set to true
   124  func (c *Container) Close() {
   125  	c.mutex.RLock()
   126  	defer c.mutex.RUnlock()
   127  	if c.watch {
   128  		close(c.stop)
   129  		<-c.stopped
   130  	}
   131  }
   132  
   133  // RenderToString renders the template referenced with the template name using
   134  // the data provided and return a string with the result
   135  func (c *Container) RenderToString(templateName string, data Data) (string, error) {
   136  	var text bytes.Buffer
   137  	if err := c.Render(&text, templateName, data); err != nil {
   138  		return "", err
   139  	}
   140  	return text.String(), nil
   141  }
   142  
   143  // RenderToString renders the template referenced with the template name using
   144  // the data provided and write it to the writer provided
   145  func (c *Container) Render(w io.Writer, templateName string, data Data) error {
   146  	c.mutex.RLock()
   147  	htmlTemplates := c.templates
   148  	c.mutex.RUnlock()
   149  
   150  	if err := htmlTemplates.ExecuteTemplate(w, templateName, data); err != nil {
   151  		return err
   152  	}
   153  
   154  	return nil
   155  }