Files
zeekay 8feb24ea1f One name for this fork, and it is ours
The server module named github.com/hanzoai/team/server/v8 — a path that
resolves to the retired workspace's repository, not this one — while
server/public still published github.com/mattermost/mattermost/server/public.
Both are github.com/hanzoteam/server/... now, which is where this code
actually lives.

That second half is what made the binary unbootable. Upstream's public path
stayed claimed by upstream, mattermost-plugin-ai imported it, and the build
linked public/model twice; public/plugin's gob.Register then ran twice under
one name and panicked at init, before main. Reverting the rename traded the
brand for a working binary. Rebranding the plugin alongside it needs no such
trade: the agents fork is github.com/hanzoteam/agents/v2 against our public
module, so no module named mattermost/mattermost is in the graph at all.

Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and
it is ours.
2026-08-28 23:35:36 -07:00

260 lines
9.7 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/server/public/shared/mlog"
"github.com/hanzoteam/server/server/v8/channels/utils/fileutils"
)
// getSubpathScript renders the inline script that defines window.publicPath to change how webpack loads assets.
func getSubpathScript(subpath string) string {
if subpath == "" || subpath == "/" {
return ""
}
newPath := path.Join(subpath, "static") + "/"
return fmt.Sprintf("window.publicPath='%s'", newPath)
}
// getConcurrentReactScript renders the inline script that defines window.enableConcurrentReact to change how React
// initializes the web app.
func getConcurrentReactScript(enableConcurrentReact bool) string {
if !enableConcurrentReact {
return ""
}
return "window.enableConcurrentReact=true"
}
// GetScriptHash computes the script-src addition required for an inline script injected into the root.html.
func GetScriptHash(script string) string {
// No hash is required when there's no script
if script == "" {
return ""
}
scriptHash := sha256.Sum256([]byte(script))
return fmt.Sprintf(" 'sha256-%s'", base64.StdEncoding.EncodeToString(scriptHash[:]))
}
// GetStaticScriptHashes computes the combined script-src additions required for the inline scripts injected
// into root.html to bypass CSP protections.
func GetStaticScriptHashes(subpath string, enableConcurrentReact bool) string {
return GetScriptHash(getSubpathScript(subpath)) + GetScriptHash(getConcurrentReactScript(enableConcurrentReact))
}
// UpdateAssetsSubpathInDir rewrites static assets in the given directory based on configuration options.
// The following changes are made:
// - HTML and CSS files are rewritten to assume the application is hosted at the given subpath instead of at the root.
// - HTML is rewritten to add the enableConcurrentReact setting, needed while loading the web app. If omitted, the
// existing value is preserved.
//
// No changes are written unless necessary.
func UpdateAssetsSubpathInDir(subpath, directory string, enableConcurrentReactPtr *bool) error {
if subpath == "" {
subpath = "/"
}
// Resolve the static directory
staticDir, found := fileutils.FindDir(directory)
if !found {
return errors.New("failed to find client dir")
}
staticDir, err := filepath.EvalSymlinks(staticDir)
if err != nil {
return errors.Wrapf(err, "failed to resolve symlinks to %s", staticDir)
}
// Read the old root.html file
rootHTMLPath := filepath.Join(staticDir, "root.html")
oldRootHTML, err := os.ReadFile(rootHTMLPath)
if err != nil {
return errors.Wrap(err, "failed to open root.html")
}
oldSubpath := "/"
// Determine if a previous subpath had already been rewritten into the assets.
reWebpackPublicPathScript := regexp.MustCompile("window.publicPath='([^']+/)static/'")
alreadyRewritten := false
if matches := reWebpackPublicPathScript.FindStringSubmatch(string(oldRootHTML)); matches != nil {
oldSubpath = matches[1]
alreadyRewritten = true
}
// Determine the old and new paths
pathToReplace := path.Join(oldSubpath, "static") + "/"
newPath := path.Join(subpath, "static") + "/"
// Keep the previous value of enableConcurrentReact if it isn't provided
enableConcurrentReact := false
if enableConcurrentReactPtr == nil {
reConcurrentReactScript := regexp.MustCompile("window.enableConcurrentReact=(true|false)")
if matches := reConcurrentReactScript.FindSubmatch(oldRootHTML); matches != nil {
if value, convErr := strconv.ParseBool(string(matches[1])); convErr == nil {
enableConcurrentReact = value
}
}
} else {
enableConcurrentReact = *enableConcurrentReactPtr
}
// Update the root.html file
if err := updateRootFile(string(oldRootHTML), rootHTMLPath, alreadyRewritten, pathToReplace, newPath, subpath, enableConcurrentReact); err != nil {
return fmt.Errorf("failed to update root.html: %w", err)
}
// Update the manifest.json and *.css files
if err := updateManifestAndCSSFiles(staticDir, pathToReplace, newPath, subpath); err != nil {
return fmt.Errorf("failed to update manifest.json and *.css files: %w", err)
}
return nil
}
func updateRootFile(oldRootHTML string, rootHTMLPath string, alreadyRewritten bool, pathToReplace, newPath, subpath string, enableConcurrentReact bool) error {
newRootHTML := oldRootHTML
reCSP := regexp.MustCompile(`<meta http-equiv="Content-Security-Policy" content="script-src 'self'([^"]*)">`)
if results := reCSP.FindAllString(newRootHTML, -1); len(results) == 0 {
return fmt.Errorf("failed to find 'Content-Security-Policy' meta tag to rewrite")
}
newRootHTML = reCSP.ReplaceAllLiteralString(newRootHTML, fmt.Sprintf(
`<meta http-equiv="Content-Security-Policy" content="script-src 'self'%s">`,
GetStaticScriptHashes(subpath, enableConcurrentReact),
))
// Rewrite the root.html references to `/static/*` to include the given subpath.
// This potentially includes a previously injected inline script that needs to
// be updated (and isn't covered by the cases above).
newRootHTML = strings.Replace(newRootHTML, pathToReplace, newPath, -1)
publicPathInWindowsScriptRegex := regexp.MustCompile(`(?s)<script id="publicPathInWindowScript">(.*?)</script>`)
if alreadyRewritten && subpath == "/" {
// Remove window global publicPath definition if subpath is root
newRootHTML = publicPathInWindowsScriptRegex.ReplaceAllLiteralString(newRootHTML, "<script id=\"publicPathInWindowScript\"></script>")
} else if !alreadyRewritten && subpath != "/" {
// Inject the script to define `window.publicPath` for the specified subpath
subpathScript := getSubpathScript(subpath)
newRootHTML = publicPathInWindowsScriptRegex.ReplaceAllLiteralString(newRootHTML, fmt.Sprintf("<script id=\"publicPathInWindowScript\">%s</script>", subpathScript))
}
// Inject (or clear) the script defining `window.enableConcurrentReact` to match the feature flag.
concurrentReactScriptRegex := regexp.MustCompile(`(?s)<script id="enableConcurrentReactScript">(.*?)</script>`)
newRootHTML = concurrentReactScriptRegex.ReplaceAllLiteralString(newRootHTML, fmt.Sprintf("<script id=\"enableConcurrentReactScript\">%s</script>", getConcurrentReactScript(enableConcurrentReact)))
if newRootHTML == oldRootHTML {
mlog.Debug("No need to rewrite unmodified root.html", mlog.String("from_subpath", pathToReplace), mlog.String("to_subpath", newPath))
return nil
}
mlog.Debug("Rewriting root.html", mlog.String("from_subpath", pathToReplace), mlog.String("to_subpath", newPath))
// Write out the updated root.html.
if err := os.WriteFile(rootHTMLPath, []byte(newRootHTML), 0); err != nil {
return errors.Wrapf(err, "failed to update root.html with subpath %s", subpath)
}
return nil
}
func updateManifestAndCSSFiles(staticDir, pathToReplace, newPath, subpath string) error {
if pathToReplace == newPath {
mlog.Debug("No need to rewrite unmodified manifest.json and *.css files", mlog.String("from_subpath", pathToReplace), mlog.String("to_subpath", newPath))
return nil
}
mlog.Debug("Rewriting manifest.json and *.css files", mlog.String("from_subpath", pathToReplace), mlog.String("to_subpath", newPath))
// Rewrite the manifest.json and *.css references to `/static/*` (or a previously rewritten subpath).
err := filepath.Walk(staticDir, func(walkPath string, info os.FileInfo, err error) error {
if filepath.Base(walkPath) == "manifest.json" || filepath.Ext(walkPath) == ".css" {
old, err := os.ReadFile(walkPath)
if err != nil {
return errors.Wrapf(err, "failed to open %s", walkPath)
}
n := strings.Replace(string(old), pathToReplace, newPath, -1)
if err = os.WriteFile(walkPath, []byte(n), 0); err != nil {
return errors.Wrapf(err, "failed to update %s with subpath %s", walkPath, subpath)
}
}
return nil
})
if err != nil {
return errors.Wrapf(err, "error walking %s", staticDir)
}
return nil
}
// UpdateAssetsSubpath rewrites assets in the /client directory to assume the application is hosted
// at the given subpath instead of at the root. No changes are written unless necessary.
func UpdateAssetsSubpath(subpath string, enableConcurrentReact *bool) error {
return UpdateAssetsSubpathInDir(subpath, model.ClientDir, enableConcurrentReact)
}
// UpdateAssetsSubpathFromConfig uses UpdateAssetsSubpath and any path defined in the SiteURL.
func UpdateAssetsSubpathFromConfig(config *model.Config) error {
// Don't rewrite in development environments, since webpack in developer mode constantly
// updates the assets and must be configured separately.
if model.BuildNumber == "dev" {
mlog.Debug("Skipping update to assets subpath since dev build")
return nil
}
// Similarly, don't rewrite during a CI build, or a local testing build, when the assets may not even be present.
if os.Getenv("IS_CI") == "true" || os.Getenv("IS_LOCAL_TESTING") == "true" {
mlog.Debug("Skipping update to assets subpath since CI build")
return nil
}
subpath, err := GetSubpathFromConfig(config)
if err != nil {
return err
}
enableConcurrentReact := false
if config != nil && config.FeatureFlags != nil {
enableConcurrentReact = config.FeatureFlags.EnableConcurrentReact
}
return UpdateAssetsSubpath(subpath, &enableConcurrentReact)
}
func GetSubpathFromConfig(config *model.Config) (string, error) {
if config == nil {
return "", errors.New("no config provided")
} else if config.ServiceSettings.SiteURL == nil {
return "/", nil
}
u, err := url.Parse(*config.ServiceSettings.SiteURL)
if err != nil {
return "", errors.Wrap(err, "failed to parse SiteURL from config")
}
if u.Path == "" {
return "/", nil
}
return path.Clean(u.Path), nil
}