github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/web/apps/serve.go (about)

     1  package apps
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"html/template"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  
    15  	"github.com/cozy/cozy-stack/model/app"
    16  	"github.com/cozy/cozy-stack/model/feature"
    17  	"github.com/cozy/cozy-stack/model/instance"
    18  	"github.com/cozy/cozy-stack/model/intent"
    19  	"github.com/cozy/cozy-stack/model/permission"
    20  	"github.com/cozy/cozy-stack/model/session"
    21  	csettings "github.com/cozy/cozy-stack/model/settings"
    22  	"github.com/cozy/cozy-stack/model/sharing"
    23  	"github.com/cozy/cozy-stack/pkg/appfs"
    24  	"github.com/cozy/cozy-stack/pkg/assets"
    25  	"github.com/cozy/cozy-stack/pkg/config/config"
    26  	"github.com/cozy/cozy-stack/pkg/consts"
    27  	"github.com/cozy/cozy-stack/pkg/couchdb"
    28  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    29  	"github.com/cozy/cozy-stack/pkg/registry"
    30  	"github.com/cozy/cozy-stack/web/auth"
    31  	"github.com/cozy/cozy-stack/web/middlewares"
    32  	"github.com/cozy/cozy-stack/web/settings"
    33  	"github.com/cozy/cozy-stack/web/statik"
    34  	"github.com/labstack/echo/v4"
    35  )
    36  
    37  // Serve is an handler for serving files from the VFS for a client-side app
    38  func Serve(c echo.Context) error {
    39  	method := c.Request().Method
    40  	if method != "GET" && method != "HEAD" {
    41  		return echo.NewHTTPError(http.StatusMethodNotAllowed, "Method "+method+" not allowed")
    42  	}
    43  
    44  	i := middlewares.GetInstance(c)
    45  	slug := c.Get("slug").(string)
    46  
    47  	if !i.OnboardingFinished {
    48  		return c.Redirect(http.StatusFound, i.PageURL("/", nil))
    49  	}
    50  
    51  	webapp, err := app.GetWebappBySlug(i, slug)
    52  	if err != nil {
    53  		if errors.Is(err, app.ErrNotFound) {
    54  			return handleAppNotFound(c, i, slug)
    55  		}
    56  		return err
    57  	}
    58  
    59  	route, file := webapp.FindRoute(path.Clean(c.Request().URL.Path))
    60  
    61  	if webapp.FromAppsDir {
    62  		// Save permissions in couchdb before loading an index page
    63  		if file == "" && webapp.Permissions() != nil {
    64  			_ = permission.ForceWebapp(i, webapp.Slug(), webapp.Permissions())
    65  		}
    66  
    67  		fs := app.FSForAppDir(slug)
    68  		return ServeAppFile(c, i, fs, webapp)
    69  	}
    70  
    71  	if file == "" || file == route.Index {
    72  		if !route.Public {
    73  			if handled, err := middlewares.CheckOAuthClientsLimitExceeded(c); handled {
    74  				return err
    75  			}
    76  		}
    77  
    78  		webapp = app.DoLazyUpdate(i, webapp, app.Copier(consts.WebappType, i), i.Registries()).(*app.WebappManifest)
    79  	}
    80  
    81  	switch webapp.State() {
    82  	case app.Installed:
    83  		// This legacy "installed" state is not used anymore with the addition
    84  		// of the registry. Change the webapp state to "ready" and serve the app
    85  		// file.
    86  		webapp.SetState(app.Ready)
    87  		if err := webapp.Update(i, nil); err != nil {
    88  			return err
    89  		}
    90  		fallthrough
    91  	case app.Ready:
    92  		return ServeAppFile(c, i, app.AppsFileServer(i), webapp)
    93  	default:
    94  		return echo.NewHTTPError(http.StatusServiceUnavailable, "Application is not ready")
    95  	}
    96  }
    97  
    98  // handleAppNotFound is used to render the error page when the user wants to
    99  // access an app that is not yet installed
   100  func handleAppNotFound(c echo.Context, i *instance.Instance, slug string) error {
   101  	// Used for the "collect" => "home" renaming
   102  	if slug == "collect" {
   103  		return c.Redirect(http.StatusMovedPermanently, i.DefaultRedirection().String())
   104  	}
   105  	// Used for the deprecated "onboarding" app
   106  	if slug == "onboarding" {
   107  		return c.Redirect(http.StatusMovedPermanently, i.DefaultRedirection().String())
   108  	}
   109  	i.Logger().WithNamespace("apps").Infof("App not found: %s", slug)
   110  	if _, err := registry.GetApplication(slug, i.Registries()); err != nil {
   111  		return app.ErrNotFound
   112  	}
   113  	if _, err := app.GetWebappBySlug(i, consts.StoreSlug); err != nil {
   114  		return app.ErrNotFound
   115  	}
   116  	u := i.SubDomain(consts.StoreSlug)
   117  	u.Fragment = "/discover/" + slug
   118  	return c.Redirect(http.StatusTemporaryRedirect, u.String())
   119  }
   120  
   121  // handleIntent will allow iframes from another app if the current app is
   122  // opened as an intent
   123  func handleIntent(c echo.Context, i *instance.Instance, slug, intentID string) {
   124  	intent := &intent.Intent{}
   125  	if err := couchdb.GetDoc(i, consts.Intents, intentID, intent); err != nil {
   126  		return
   127  	}
   128  	allowed := false
   129  	for _, service := range intent.Services {
   130  		if slug == service.Slug {
   131  			allowed = true
   132  		}
   133  	}
   134  	if !allowed {
   135  		return
   136  	}
   137  	parts := strings.SplitN(intent.Client, "/", 2)
   138  	if len(parts) < 2 || parts[0] != consts.Apps {
   139  		return
   140  	}
   141  	from := i.SubDomain(parts[1]).String()
   142  	if !config.GetConfig().CSPDisabled {
   143  		middlewares.AppendCSPRule(c, "frame-ancestors", from)
   144  	}
   145  }
   146  
   147  // ServeAppFile will serve the requested file using the specified application
   148  // manifest and appfs.FileServer context.
   149  //
   150  // It can be used to serve file application in another context than the VFS,
   151  // for instance for tests or development purposes where we want to serve an
   152  // application that is not installed on the user's instance. However this
   153  // procedure should not be used for standard applications, use the Serve method
   154  // for that.
   155  func ServeAppFile(c echo.Context, i *instance.Instance, fs appfs.FileServer, webapp *app.WebappManifest) error {
   156  	slug := webapp.Slug()
   157  	route, file := webapp.FindRoute(path.Clean(c.Request().URL.Path))
   158  	if route.NotFound() {
   159  		return echo.NewHTTPError(http.StatusNotFound, "Page not found")
   160  	}
   161  	if file == "" {
   162  		file = route.Index
   163  	}
   164  
   165  	sess, isLoggedIn := middlewares.GetSession(c)
   166  	if code := c.QueryParam("session_code"); code != "" {
   167  		// XXX we should always clear the session code to avoid it being
   168  		// reused, even if the user is already logged in and we don't want to
   169  		// create a new session
   170  		if checked := i.CheckAndClearSessionCode(code); checked && !isLoggedIn {
   171  			sessionID, err := auth.SetCookieForNewSession(c, session.NormalRun)
   172  			req := c.Request()
   173  			if err == nil {
   174  				if err = session.StoreNewLoginEntry(i, sessionID, "", req, "session_code", false); err != nil {
   175  					i.Logger().Errorf("Could not store session history %q: %s", sessionID, err)
   176  				}
   177  			}
   178  			redirect := req.URL
   179  			redirect.RawQuery = ""
   180  			return c.Redirect(http.StatusSeeOther, redirect.String())
   181  		}
   182  	}
   183  
   184  	filepath := path.Join("/", route.Folder, file)
   185  	isRobotsTxt := filepath == "/robots.txt"
   186  
   187  	if !route.Public && !isLoggedIn {
   188  		if isRobotsTxt {
   189  			if f, ok := assets.Get("/robots.txt", i.ContextName); ok {
   190  				_, err := io.Copy(c.Response(), f.Reader())
   191  				return err
   192  			}
   193  		}
   194  		if file != route.Index {
   195  			return echo.NewHTTPError(http.StatusUnauthorized, "You must be authenticated")
   196  		}
   197  		reqURL := c.Request().URL
   198  		subdomain := i.SubDomain(slug)
   199  		subdomain.Path = reqURL.Path
   200  		subdomain.RawQuery = reqURL.RawQuery
   201  		subdomain.Fragment = reqURL.Fragment
   202  		params := url.Values{
   203  			"redirect": {subdomain.String()},
   204  		}
   205  		if jwt := c.QueryParam("jwt"); jwt != "" {
   206  			params.Add("jwt", jwt)
   207  		}
   208  		return c.Redirect(http.StatusFound, i.PageURL("/auth/login", params))
   209  	}
   210  
   211  	version := webapp.Version()
   212  	shasum := webapp.Checksum()
   213  
   214  	if file != route.Index {
   215  		// If file is not the index, it is considered an asset of the application
   216  		// (JS, image, ...). For theses assets we check if it contains an unique
   217  		// identifier to help caching. In such case, a long cache (1 year) is set.
   218  		//
   219  		// A unique identifier is matched when the file base contains a "long"
   220  		// hexadecimal subpart between '.', of at least 10 characters: for instance
   221  		// "app.badf00dbadf00d.js".
   222  		if _, id := statik.ExtractAssetID(file); id != "" {
   223  			c.Response().Header().Set("Cache-Control", "max-age=31536000, immutable")
   224  		}
   225  
   226  		err := fs.ServeFileContent(c.Response(), c.Request(), slug, version, shasum, filepath)
   227  		if os.IsNotExist(err) {
   228  			if isRobotsTxt {
   229  				if f, ok := assets.Get("/robots.txt", i.ContextName); ok {
   230  					_, err = io.Copy(c.Response(), f.Reader())
   231  					return err
   232  				}
   233  			}
   234  			return echo.NewHTTPError(http.StatusNotFound, "Asset not found")
   235  		}
   236  		if err != nil {
   237  			return echo.NewHTTPError(http.StatusInternalServerError, err)
   238  		}
   239  
   240  		return nil
   241  	}
   242  
   243  	if !isLoggedIn {
   244  		doc, err := i.SettingsDocument()
   245  		if err == nil {
   246  			if to, ok := doc.M["moved_to"].(string); ok && to != "" {
   247  				subdomainType, _ := doc.M["moved_to_subdomain_type"].(string)
   248  				return renderMovedLink(c, i, to, subdomainType)
   249  			}
   250  		}
   251  	}
   252  
   253  	// For share by link, show the password page if it is password protected.
   254  	code := c.QueryParam("sharecode")
   255  	token, err := middlewares.TransformShortcodeToJWT(i, code)
   256  	if err == nil {
   257  		claims, err := middlewares.ExtractClaims(c, i, token)
   258  		if err == nil && claims.AudienceString() == consts.ShareAudience {
   259  			pdoc, err := permission.GetForShareCode(i, token)
   260  			if err == nil && pdoc.Password != nil && !middlewares.HasCookieForPassword(c, i, pdoc.ID()) {
   261  				return renderPasswordPage(c, i, pdoc.ID())
   262  			}
   263  		}
   264  	}
   265  
   266  	if intentID := c.QueryParam("intent"); intentID != "" {
   267  		handleIntent(c, i, slug, intentID)
   268  	}
   269  
   270  	// For index file, we inject the locale, the stack domain, and a token if the
   271  	// user is connected
   272  	content, err := fs.Open(slug, version, shasum, filepath)
   273  	if err != nil {
   274  		return err
   275  	}
   276  	defer content.Close()
   277  
   278  	buf, err := io.ReadAll(content)
   279  	if err != nil {
   280  		return err
   281  	}
   282  
   283  	// XXX: Force include Warnings template in all app indexes
   284  	tmplText := string(buf)
   285  	if closeTagIdx := strings.Index(tmplText, "</head>"); closeTagIdx >= 0 {
   286  		tmplText = tmplText[:closeTagIdx] + "\n{{.Warnings}}\n" + tmplText[closeTagIdx:]
   287  	} else {
   288  		needsOpenTag := true
   289  		if openTagIdx := strings.Index(tmplText, "<head>"); openTagIdx >= 0 {
   290  			needsOpenTag = false
   291  		}
   292  
   293  		if bodyTagIdx := strings.Index(tmplText, "<body>"); bodyTagIdx >= 0 {
   294  			before := tmplText[:bodyTagIdx]
   295  			after := tmplText[bodyTagIdx:]
   296  
   297  			tmplText = before
   298  
   299  			if needsOpenTag {
   300  				tmplText += "\n<head>"
   301  			}
   302  
   303  			tmplText += "\n{{.Warnings}}\n</head>\n" + after
   304  		}
   305  	}
   306  
   307  	tmpl, err := template.New(file).Parse(tmplText)
   308  	if err != nil {
   309  		i.Logger().WithNamespace("apps").Warnf("%s cannot be parsed as a template: %s", file, err)
   310  		return fs.ServeFileContent(c.Response(), c.Request(), slug, version, shasum, filepath)
   311  	}
   312  
   313  	sessID := ""
   314  	if isLoggedIn {
   315  		sessID = sess.ID()
   316  	}
   317  	params := buildServeParams(c, i, webapp, isLoggedIn, sessID)
   318  
   319  	generated := &bytes.Buffer{}
   320  	if err := tmpl.Execute(generated, params); err != nil {
   321  		i.Logger().WithNamespace("apps").Warnf("%s cannot be interpreted as a template: %s", file, err)
   322  		return c.Render(http.StatusInternalServerError, "error.html", echo.Map{
   323  			"Domain":       i.ContextualDomain(),
   324  			"ContextName":  i.ContextName,
   325  			"Locale":       i.Locale,
   326  			"Title":        i.TemplateTitle(),
   327  			"Favicon":      middlewares.Favicon(i),
   328  			"Illustration": "/images/generic-error.svg",
   329  			"Error":        "Error Application not supported Message",
   330  			"SupportEmail": i.SupportEmailAddress(),
   331  		})
   332  	}
   333  
   334  	res := c.Response()
   335  	res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
   336  	res.Header().Set("Cache-Control", "private, no-store, must-revalidate")
   337  	res.WriteHeader(http.StatusOK)
   338  	_, err = io.Copy(res, generated)
   339  	return err
   340  }
   341  
   342  func buildServeParams(
   343  	c echo.Context,
   344  	inst *instance.Instance,
   345  	webapp *app.WebappManifest,
   346  	isLoggedIn bool,
   347  	sessID string,
   348  ) serveParams {
   349  	token := getServeToken(c, inst, webapp, isLoggedIn, sessID)
   350  	tracking := false
   351  	settings, err := inst.SettingsDocument()
   352  	if err == nil {
   353  		if t, ok := settings.M["tracking"].(string); ok {
   354  			tracking = t == "true"
   355  		}
   356  	}
   357  	var subdomainsType string
   358  	switch config.GetConfig().Subdomains {
   359  	case config.FlatSubdomains:
   360  		subdomainsType = "flat"
   361  	case config.NestedSubdomains:
   362  		subdomainsType = "nested"
   363  	}
   364  
   365  	return serveParams{
   366  		Token:      token,
   367  		SubDomain:  subdomainsType,
   368  		Tracking:   tracking,
   369  		webapp:     webapp,
   370  		instance:   inst,
   371  		isLoggedIn: isLoggedIn,
   372  	}
   373  }
   374  
   375  func getServeToken(
   376  	c echo.Context,
   377  	inst *instance.Instance,
   378  	webapp *app.WebappManifest,
   379  	isLoggedIn bool,
   380  	sessID string,
   381  ) string {
   382  	sharecode := c.QueryParam("sharecode")
   383  	if sharecode == "" {
   384  		if isLoggedIn {
   385  			return inst.BuildAppToken(webapp.Slug(), sessID)
   386  		}
   387  		return ""
   388  	}
   389  
   390  	// XXX The sharecode can be used for share by links, or for Cozy to Cozy
   391  	// sharings. When it is used for a Cozy to Cozy sharing, it can be for a
   392  	// preview token, and we need to upgrade it to an interact token if the
   393  	// member has a known Cozy URL. We do this upgrade when serving the preview
   394  	// page, not when sending the invitation link by mail, because we want the
   395  	// same link to work (and be upgraded) after the user has accepted the
   396  	// sharing.
   397  	token, pdoc, err := permission.GetTokenAndPermissionsFromShortcode(inst, sharecode)
   398  	if err != nil || pdoc.Type != permission.TypeSharePreview {
   399  		return sharecode
   400  	}
   401  	sharingID := strings.Split(pdoc.SourceID, "/")
   402  	sharingDoc, err := sharing.FindSharing(inst, sharingID[1])
   403  	if err != nil || sharingDoc.ReadOnlyRules() {
   404  		return token
   405  	}
   406  	m, err := sharingDoc.FindMemberBySharecode(inst, token)
   407  	if err != nil {
   408  		return token
   409  	}
   410  	if m.Instance != "" && !m.ReadOnly && m.Status != sharing.MemberStatusRevoked {
   411  		memberIndex := 0
   412  		for i := range sharingDoc.Members {
   413  			if sharingDoc.Members[i].Instance == m.Instance {
   414  				memberIndex = i
   415  			}
   416  		}
   417  		interact, err := sharingDoc.GetInteractCode(inst, m, memberIndex)
   418  		if err == nil {
   419  			return interact
   420  		}
   421  	}
   422  	return token
   423  }
   424  
   425  func renderMovedLink(c echo.Context, i *instance.Instance, to, subdomainType string) error {
   426  	name, _ := csettings.PublicName(i)
   427  	link := *c.Request().URL
   428  	if u, err := url.Parse(to); err == nil {
   429  		parts := strings.SplitN(c.Request().Host, ".", 2)
   430  		app := parts[0]
   431  		if config.GetConfig().Subdomains == config.FlatSubdomains {
   432  			parts = strings.SplitN(app, "-", 2)
   433  			app = parts[len(parts)-1]
   434  		}
   435  		if subdomainType == "nested" {
   436  			link.Host = app + "." + u.Host
   437  		} else {
   438  			parts := strings.SplitN(u.Host, ".", 2)
   439  			link.Host = parts[0] + "-" + app + "." + parts[1]
   440  		}
   441  		link.Scheme = u.Scheme
   442  	}
   443  
   444  	return c.Render(http.StatusGone, "move_link.html", echo.Map{
   445  		"Domain":      i.ContextualDomain(),
   446  		"ContextName": i.ContextName,
   447  		"Locale":      i.Locale,
   448  		"Title":       i.Translate("Move Link Title", name),
   449  		"ThemeCSS":    middlewares.ThemeCSS(i),
   450  		"Favicon":     middlewares.Favicon(i),
   451  		"Link":        link.String(),
   452  	})
   453  }
   454  
   455  func renderPasswordPage(c echo.Context, inst *instance.Instance, permID string) error {
   456  	return c.Render(http.StatusUnauthorized, "share_by_link_password.html", echo.Map{
   457  		"Action":      inst.PageURL("/auth/share-by-link/password", nil),
   458  		"Domain":      inst.ContextualDomain(),
   459  		"ContextName": inst.ContextName,
   460  		"Locale":      inst.Locale,
   461  		"Title":       inst.TemplateTitle(),
   462  		"ThemeCSS":    middlewares.ThemeCSS(inst),
   463  		"Favicon":     middlewares.Favicon(inst),
   464  		"PermID":      permID,
   465  	})
   466  }
   467  
   468  // serveParams is a struct used for rendering the index.html of webapps. A
   469  // struct is used, and not a map, to have some methods declared on it. It
   470  // allows to be lazy when constructing the paths of the assets: if an asset is
   471  // not used in the template, the method won't be called and the stack can avoid
   472  // checking if this asset is dynamically overridden in this instance context.
   473  type serveParams struct {
   474  	Token      string
   475  	SubDomain  string
   476  	Tracking   bool
   477  	webapp     *app.WebappManifest
   478  	instance   *instance.Instance
   479  	isLoggedIn bool
   480  }
   481  
   482  func (s serveParams) CozyData() (string, error) {
   483  	data := map[string]interface{}{
   484  		"token":     s.Token,
   485  		"domain":    s.Domain(),
   486  		"subdomain": s.SubDomain,
   487  		"tracking":  s.Tracking,
   488  		"locale":    s.Locale(),
   489  		"app": map[string]interface{}{
   490  			"editor": s.AppEditor(),
   491  			"name":   s.AppName(),
   492  			"prefix": s.AppNamePrefix(),
   493  			"slug":   s.AppSlug(),
   494  			"icon":   s.IconPath(),
   495  		},
   496  		"flags":        s.GetFlags(),
   497  		"capabilities": s.GetCapabilities(),
   498  	}
   499  	bytes, err := json.Marshal(data)
   500  
   501  	if err != nil {
   502  		return "", err
   503  	}
   504  
   505  	return string(bytes), nil
   506  }
   507  
   508  func (s serveParams) Domain() string {
   509  	return s.instance.ContextualDomain()
   510  }
   511  
   512  func (s serveParams) ContextName() string {
   513  	return s.instance.ContextName
   514  }
   515  
   516  func (s serveParams) Locale() string {
   517  	return s.instance.Locale
   518  }
   519  
   520  func (s serveParams) AppSlug() string {
   521  	return s.webapp.Slug()
   522  }
   523  
   524  func (s serveParams) AppName() string {
   525  	return s.webapp.NameLocalized(s.instance.Locale)
   526  }
   527  
   528  func (s serveParams) AppEditor() string {
   529  	return s.webapp.Editor()
   530  }
   531  
   532  func (s serveParams) AppNamePrefix() string {
   533  	return s.webapp.NamePrefix()
   534  }
   535  
   536  func (s serveParams) IconPath() string {
   537  	return s.webapp.Icon()
   538  }
   539  
   540  func (s serveParams) Capabilities() (string, error) {
   541  	bytes, err := json.Marshal(s.GetCapabilities())
   542  	if err != nil {
   543  		return "", err
   544  	}
   545  	return string(bytes), nil
   546  }
   547  
   548  func (s serveParams) GetCapabilities() jsonapi.Object {
   549  	capabilities := settings.NewCapabilities(s.instance)
   550  	capabilities.SetID("")
   551  	return capabilities
   552  }
   553  
   554  func (s serveParams) Flags() (string, error) {
   555  	flags, err := feature.GetFlags(s.instance)
   556  	if err != nil {
   557  		return "{}", err
   558  	}
   559  	bytes, err := json.Marshal(flags)
   560  	if err != nil {
   561  		return "", err
   562  	}
   563  	return string(bytes), nil
   564  }
   565  
   566  func (s serveParams) GetFlags() *feature.Flags {
   567  	flags, err := feature.GetFlags(s.instance)
   568  	if err != nil {
   569  		flags = &feature.Flags{
   570  			M: map[string]interface{}{},
   571  		}
   572  	}
   573  	return flags
   574  }
   575  
   576  func (s serveParams) CozyBar() (template.HTML, error) {
   577  	return cozybarHTML(s.instance, s.isLoggedIn)
   578  }
   579  
   580  func (s serveParams) CozyClientJS() (template.HTML, error) {
   581  	return cozyclientjsHTML(s.instance)
   582  }
   583  
   584  func (s serveParams) CozyFonts() template.HTML {
   585  	return middlewares.CozyFonts(s.instance)
   586  }
   587  
   588  func (s serveParams) ThemeCSS() template.HTML {
   589  	return middlewares.ThemeCSS(s.instance)
   590  }
   591  
   592  func (s serveParams) Favicon() template.HTML {
   593  	return middlewares.Favicon(s.instance)
   594  }
   595  
   596  func (s serveParams) DefaultWallpaper() string {
   597  	return statik.AssetPath(
   598  		s.instance.ContextualDomain(),
   599  		"/images/default-wallpaper.jpg",
   600  		s.instance.ContextName)
   601  }
   602  
   603  func (s serveParams) Warnings() (template.HTML, error) {
   604  	return warningsHTML(s.instance, s.isLoggedIn)
   605  }
   606  
   607  var clientTemplate *template.Template
   608  var barTemplate *template.Template
   609  var warningsTemplate *template.Template
   610  
   611  // BuildTemplates ensure that cozy-client-js and the bar can be injected in templates
   612  func BuildTemplates() {
   613  	clientTemplate = template.Must(template.New("cozy-client-js").Funcs(middlewares.FuncsMap).Parse(`` +
   614  		`<script src="{{asset .Domain "/js/cozy-client.min.js" .ContextName}}"></script>`,
   615  	))
   616  
   617  	barTemplate = template.Must(template.New("cozy-bar").Funcs(middlewares.FuncsMap).Parse(`
   618  <link rel="stylesheet" type="text/css" href="{{asset .Domain "/fonts/fonts.css" .ContextName}}">
   619  <link rel="stylesheet" type="text/css" href="{{asset .Domain "/css/cozy-bar.min.css" .ContextName}}">
   620  <script src="{{asset .Domain "/js/cozy-bar.min.js" .ContextName}}"></script>`,
   621  	))
   622  
   623  	warningsTemplate = template.Must(template.New("warnings").Funcs(middlewares.FuncsMap).Parse(`
   624  {{if .LoggedIn}}
   625  {{range .Warnings}}
   626  <meta name="user-action-required" data-title="{{ .Title }}" data-code="{{ .Code }}" data-detail="{{ .Detail }}" {{with .Links}}{{with .Self}}data-links="{{ . }}"{{end}}{{end}} />
   627  {{end}}
   628  {{end}}`,
   629  	))
   630  }
   631  
   632  func cozyclientjsHTML(i *instance.Instance) (template.HTML, error) {
   633  	buf := new(bytes.Buffer)
   634  	err := clientTemplate.Execute(buf, echo.Map{
   635  		"Domain":      i.ContextualDomain(),
   636  		"ContextName": i.ContextName,
   637  	})
   638  	if err != nil {
   639  		return "", err
   640  	}
   641  	return template.HTML(buf.String()), nil
   642  }
   643  
   644  func cozybarHTML(i *instance.Instance, loggedIn bool) (template.HTML, error) {
   645  	buf := new(bytes.Buffer)
   646  	err := barTemplate.Execute(buf, echo.Map{
   647  		"Domain":      i.ContextualDomain(),
   648  		"Warnings":    middlewares.ListWarnings(i),
   649  		"ContextName": i.ContextName,
   650  		"LoggedIn":    loggedIn,
   651  	})
   652  	if err != nil {
   653  		return "", err
   654  	}
   655  	return template.HTML(buf.String()), nil
   656  }
   657  
   658  func warningsHTML(i *instance.Instance, loggedIn bool) (template.HTML, error) {
   659  	buf := new(bytes.Buffer)
   660  	err := warningsTemplate.Execute(buf, echo.Map{
   661  		"Warnings": middlewares.ListWarnings(i),
   662  		"LoggedIn": loggedIn,
   663  	})
   664  	if err != nil {
   665  		return "", err
   666  	}
   667  	return template.HTML(buf.String()), nil
   668  }