package config import ( "strings" "time" "github.com/photoprism/photoprism/internal/acl" "github.com/photoprism/photoprism/internal/customize" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/pkg/colors" "github.com/photoprism/photoprism/pkg/env" "github.com/photoprism/photoprism/pkg/txt" ) type ClientType string const ( ClientPublic ClientType = "public" ClientShare ClientType = "share" ClientUser ClientType = "user" ) // ClientConfig represents HTTP client / Web UI config options. type ClientConfig struct { Mode string `json:"mode"` Name string `json:"name"` Edition string `json:"edition"` Version string `json:"version"` Copyright string `json:"copyright"` Flags string `json:"flags"` BaseUri string `json:"baseUri"` StaticUri string `json:"staticUri"` CssUri string `json:"cssUri"` JsUri string `json:"jsUri"` ManifestUri string `json:"manifestUri"` ApiUri string `json:"apiUri"` ContentUri string `json:"contentUri"` WallpaperUri string `json:"wallpaperUri"` SiteUrl string `json:"siteUrl"` SiteDomain string `json:"siteDomain"` SiteAuthor string `json:"siteAuthor"` SiteTitle string `json:"siteTitle"` SiteCaption string `json:"siteCaption"` SiteDescription string `json:"siteDescription"` SitePreview string `json:"sitePreview"` Imprint string `json:"imprint"` ImprintUrl string `json:"imprintUrl"` AppName string `json:"appName"` AppMode string `json:"appMode"` AppIcon string `json:"appIcon"` Debug bool `json:"debug"` Trace bool `json:"trace"` Test bool `json:"test"` Demo bool `json:"demo"` Sponsor bool `json:"sponsor"` ReadOnly bool `json:"readonly"` UploadNSFW bool `json:"uploadNSFW"` Public bool `json:"public"` Experimental bool `json:"experimental"` AlbumCategories []string `json:"albumCategories"` Albums entity.Albums `json:"albums"` Cameras entity.Cameras `json:"cameras"` Lenses entity.Lenses `json:"lenses"` Countries entity.Countries `json:"countries"` People entity.People `json:"people"` Thumbs ThumbSizes `json:"thumbs"` Status string `json:"status"` MapKey string `json:"mapKey"` DownloadToken string `json:"downloadToken"` PreviewToken string `json:"previewToken"` Disable ClientDisable `json:"disable"` Count ClientCounts `json:"count"` Pos ClientPosition `json:"pos"` Years Years `json:"years"` Colors []map[string]string `json:"colors"` Categories CategoryLabels `json:"categories"` Clip int `json:"clip"` Server env.Resources `json:"server"` Settings *customize.Settings `json:"settings,omitempty"` ACL acl.Grants `json:"acl,omitempty"` Ext Values `json:"ext"` } // ApplyACL updates the client config values based on the ACL and Role provided. func (c ClientConfig) ApplyACL(a acl.ACL, r acl.Role) ClientConfig { if c.Settings != nil { c.Settings = c.Settings.ApplyACL(a, r) } c.ACL = a.Grants(r) return c } // Years represents a list of years. type Years []int // ClientDisable represents disabled client features a user cannot turn back on. type ClientDisable struct { Backups bool `json:"backups"` WebDAV bool `json:"webdav"` Settings bool `json:"settings"` Places bool `json:"places"` ExifTool bool `json:"exiftool"` FFmpeg bool `json:"ffmpeg"` Raw bool `json:"raw"` Darktable bool `json:"darktable"` Rawtherapee bool `json:"rawtherapee"` Sips bool `json:"sips"` HeifConvert bool `json:"heifconvert"` TensorFlow bool `json:"tensorflow"` Faces bool `json:"faces"` Classification bool `json:"classification"` } // ClientCounts represents photo, video and album counts for the client UI. type ClientCounts struct { All int `json:"all"` Photos int `json:"photos"` Live int `json:"live"` Videos int `json:"videos"` Cameras int `json:"cameras"` Lenses int `json:"lenses"` Countries int `json:"countries"` Hidden int `json:"hidden"` Favorites int `json:"favorites"` Private int `json:"private"` Review int `json:"review"` Stories int `json:"stories"` Albums int `json:"albums"` Moments int `json:"moments"` Months int `json:"months"` Folders int `json:"folders"` Files int `json:"files"` People int `json:"people"` Places int `json:"places"` States int `json:"states"` Labels int `json:"labels"` LabelMaxPhotos int `json:"labelMaxPhotos"` } type CategoryLabels []CategoryLabel type CategoryLabel struct { LabelUID string `json:"UID"` CustomSlug string `json:"Slug"` LabelName string `json:"Name"` } type ClientPosition struct { PhotoUID string `json:"uid"` CellID string `json:"cid"` TakenAt time.Time `json:"utc"` PhotoLat float64 `json:"lat"` PhotoLng float64 `json:"lng"` } // Flags returns config flags as string slice. func (c *Config) Flags() (flags []string) { if c.Public() { flags = append(flags, "public") } if c.Debug() { flags = append(flags, "debug") } if c.Test() { flags = append(flags, "test") } if c.Demo() { flags = append(flags, "demo") } if c.Sponsor() { flags = append(flags, "sponsor") } if c.Experimental() { flags = append(flags, "experimental") } if c.ReadOnly() { flags = append(flags, "readonly") } if !c.DisableSettings() { flags = append(flags, "settings") } if !c.Settings().UI.Scrollbar { flags = append(flags, "hide-scrollbar") } return flags } // ClientPublic returns config values for use by the JavaScript UI and other clients. func (c *Config) ClientPublic() ClientConfig { if c.Public() { return c.ClientUser(true).ApplyACL(acl.Resources, acl.RoleAdmin) } a := c.ClientAssets() cfg := ClientConfig{ Settings: c.PublicSettings(), ACL: acl.Resources.Grants(acl.RoleUnauthorized), Disable: ClientDisable{ Backups: true, WebDAV: true, Settings: c.DisableSettings(), Places: c.DisablePlaces(), ExifTool: true, FFmpeg: true, Raw: true, Darktable: true, Rawtherapee: true, Sips: true, HeifConvert: true, TensorFlow: true, Faces: true, Classification: true, }, Flags: strings.Join(c.Flags(), " "), Mode: string(ClientPublic), Name: c.Name(), Edition: c.Edition(), BaseUri: c.BaseUri(""), StaticUri: c.StaticUri(), CssUri: a.AppCssUri(), JsUri: a.AppJsUri(), ApiUri: c.ApiUri(), ContentUri: c.ContentUri(), SiteUrl: c.SiteUrl(), SiteDomain: c.SiteDomain(), SiteAuthor: c.SiteAuthor(), SiteTitle: c.SiteTitle(), SiteCaption: c.SiteCaption(), SiteDescription: c.SiteDescription(), SitePreview: c.SitePreview(), Imprint: c.Imprint(), ImprintUrl: c.ImprintUrl(), AppName: c.AppName(), AppMode: c.AppMode(), AppIcon: c.AppIcon(), WallpaperUri: c.WallpaperUri(), Version: c.Version(), Copyright: c.Copyright(), Debug: c.Debug(), Trace: c.Trace(), Test: c.Test(), Demo: c.Demo(), Sponsor: c.Sponsor(), ReadOnly: c.ReadOnly(), Public: c.Public(), Experimental: c.Experimental(), Albums: entity.Albums{}, Cameras: entity.Cameras{}, Lenses: entity.Lenses{}, Countries: entity.Countries{}, People: entity.People{}, Status: "", MapKey: "", Thumbs: Thumbs, Colors: colors.All.List(), ManifestUri: c.ClientManifestUri(), Clip: txt.ClipDefault, PreviewToken: "public", DownloadToken: "public", Ext: ClientExt(c, ClientPublic), } return cfg } // ClientShare returns reduced client config values for share link visitors. func (c *Config) ClientShare() ClientConfig { a := c.ClientAssets() cfg := ClientConfig{ Settings: c.ShareSettings(), ACL: acl.Resources.Grants(acl.RoleVisitor), Disable: ClientDisable{ Backups: true, WebDAV: c.DisableWebDAV(), Settings: c.DisableSettings(), Places: c.DisablePlaces(), ExifTool: true, FFmpeg: true, Raw: true, Darktable: true, Rawtherapee: true, Sips: true, HeifConvert: true, TensorFlow: true, Faces: true, Classification: true, }, Flags: strings.Join(c.Flags(), " "), Mode: string(ClientShare), Name: c.Name(), Edition: c.Edition(), BaseUri: c.BaseUri(""), StaticUri: c.StaticUri(), CssUri: a.AppCssUri(), JsUri: a.ShareJsUri(), ApiUri: c.ApiUri(), ContentUri: c.ContentUri(), SiteUrl: c.SiteUrl(), SiteDomain: c.SiteDomain(), SiteAuthor: c.SiteAuthor(), SiteTitle: c.SiteTitle(), SiteCaption: c.SiteCaption(), SiteDescription: c.SiteDescription(), SitePreview: c.SitePreview(), Imprint: c.Imprint(), ImprintUrl: c.ImprintUrl(), AppName: c.AppName(), AppMode: c.AppMode(), AppIcon: c.AppIcon(), WallpaperUri: c.WallpaperUri(), Version: c.Version(), Copyright: c.Copyright(), Debug: c.Debug(), Trace: c.Trace(), Test: c.Test(), Demo: c.Demo(), Sponsor: c.Sponsor(), ReadOnly: c.ReadOnly(), UploadNSFW: c.UploadNSFW(), Public: c.Public(), Experimental: c.Experimental(), Albums: entity.Albums{}, Cameras: entity.Cameras{}, Lenses: entity.Lenses{}, Countries: entity.Countries{}, People: entity.People{}, Colors: colors.All.List(), Thumbs: Thumbs, Status: c.Hub().Status, MapKey: c.Hub().MapKey(), DownloadToken: c.DownloadToken(), PreviewToken: c.PreviewToken(), ManifestUri: c.ClientManifestUri(), Clip: txt.ClipDefault, Ext: ClientExt(c, ClientShare), } return cfg } // ClientUser returns complete client config values for users with full access. func (c *Config) ClientUser(withSettings bool) ClientConfig { a := c.ClientAssets() var s *customize.Settings if withSettings { s = c.Settings() } cfg := ClientConfig{ Settings: s, Disable: ClientDisable{ Backups: c.DisableBackups(), WebDAV: c.DisableWebDAV(), Settings: c.DisableSettings(), Places: c.DisablePlaces(), ExifTool: c.DisableExifTool(), FFmpeg: c.DisableFFmpeg(), Raw: c.DisableRaw(), Darktable: c.DisableDarktable(), Rawtherapee: c.DisableRawtherapee(), Sips: c.DisableSips(), HeifConvert: c.DisableHeifConvert(), TensorFlow: c.DisableTensorFlow(), Faces: c.DisableFaces(), Classification: c.DisableClassification(), }, Flags: strings.Join(c.Flags(), " "), Mode: string(ClientUser), Name: c.Name(), Edition: c.Edition(), BaseUri: c.BaseUri(""), StaticUri: c.StaticUri(), CssUri: a.AppCssUri(), JsUri: a.AppJsUri(), ApiUri: c.ApiUri(), ContentUri: c.ContentUri(), SiteUrl: c.SiteUrl(), SiteDomain: c.SiteDomain(), SiteAuthor: c.SiteAuthor(), SiteTitle: c.SiteTitle(), SiteCaption: c.SiteCaption(), SiteDescription: c.SiteDescription(), SitePreview: c.SitePreview(), Imprint: c.Imprint(), ImprintUrl: c.ImprintUrl(), AppName: c.AppName(), AppMode: c.AppMode(), AppIcon: c.AppIcon(), WallpaperUri: c.WallpaperUri(), Version: c.Version(), Copyright: c.Copyright(), Debug: c.Debug(), Trace: c.Trace(), Test: c.Test(), Demo: c.Demo(), Sponsor: c.Sponsor(), ReadOnly: c.ReadOnly(), UploadNSFW: c.UploadNSFW(), Public: c.Public(), Experimental: c.Experimental(), Albums: entity.Albums{}, Cameras: entity.Cameras{}, Lenses: entity.Lenses{}, Countries: entity.Countries{}, People: entity.People{}, Colors: colors.All.List(), Thumbs: Thumbs, Status: c.Hub().Status, MapKey: c.Hub().MapKey(), DownloadToken: c.DownloadToken(), PreviewToken: c.PreviewToken(), ManifestUri: c.ClientManifestUri(), Clip: txt.ClipDefault, Server: env.Info(), Ext: ClientExt(c, ClientUser), } hidePrivate := c.Settings().Features.Private c.Db(). Table("photos"). Select("photo_uid, cell_id, photo_lat, photo_lng, taken_at"). Where("deleted_at IS NULL AND photo_lat <> 0 AND photo_lng <> 0"). Order("taken_at DESC"). Limit(1).Offset(0). Take(&cfg.Pos) c.Db(). Table("cameras"). Where("camera_slug <> 'zz' AND camera_slug <> ''"). Select("COUNT(*) AS cameras"). Take(&cfg.Count) c.Db(). Table("lenses"). Where("lens_slug <> 'zz' AND lens_slug <> ''"). Select("COUNT(*) AS lenses"). Take(&cfg.Count) if hidePrivate { c.Db(). Table("photos"). Select("SUM(photo_type = 'video' AND photo_quality > -1 AND photo_private = 0) AS videos, " + "SUM(photo_type = 'live' AND photo_quality > -1 AND photo_private = 0) AS live, " + "SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','animated') AND photo_private = 0 AND photo_quality > -1) AS photos, " + "SUM(photo_type IN ('image','raw','live','animated') AND photo_quality < 3 AND photo_quality > -1 AND photo_private = 0) AS review, " + "SUM(photo_favorite = 1 AND photo_private = 0 AND photo_quality > -1) AS favorites, " + "SUM(photo_private = 1 AND photo_quality > -1) AS private"). Where("photos.id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND (file_missing = 1 OR file_error <> ''))"). Where("deleted_at IS NULL"). Take(&cfg.Count) } else { c.Db(). Table("photos"). Select("SUM(photo_type = 'video' AND photo_quality > -1) AS videos, " + "SUM(photo_type = 'live' AND photo_quality > -1) AS live, " + "SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','animated') AND photo_quality > -1) AS photos, " + "SUM(photo_type IN ('image','raw','live','animated') AND photo_quality < 3 AND photo_quality > -1) AS review, " + "SUM(photo_favorite = 1 AND photo_quality > -1) AS favorites, " + "0 AS private"). Where("photos.id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND (file_missing = 1 OR file_error <> ''))"). Where("deleted_at IS NULL"). Take(&cfg.Count) } cfg.Count.All = cfg.Count.Photos + cfg.Count.Live + cfg.Count.Videos c.Db(). Table("labels"). Select("MAX(photo_count) AS label_max_photos, COUNT(*) AS labels"). Where("photo_count > 0"). Where("deleted_at IS NULL"). Where("(label_priority >= 0 OR label_favorite = 1)"). Take(&cfg.Count) if hidePrivate { c.Db(). Table("albums"). Select("SUM(album_type = ?) AS albums, SUM(album_type = ?) AS moments, SUM(album_type = ?) AS months, SUM(album_type = ?) AS states, SUM(album_type = ?) AS folders", entity.AlbumDefault, entity.AlbumMoment, entity.AlbumMonth, entity.AlbumState, entity.AlbumFolder). Where("deleted_at IS NULL AND (albums.album_type <> 'folder' OR albums.album_path IN (SELECT photos.photo_path FROM photos WHERE photos.photo_private = 0 AND photos.deleted_at IS NULL))"). Take(&cfg.Count) } else { c.Db(). Table("albums"). Select("SUM(album_type = ?) AS albums, SUM(album_type = ?) AS moments, SUM(album_type = ?) AS months, SUM(album_type = ?) AS states, SUM(album_type = ?) AS folders", entity.AlbumDefault, entity.AlbumMoment, entity.AlbumMonth, entity.AlbumState, entity.AlbumFolder). Where("deleted_at IS NULL AND (albums.album_type <> 'folder' OR albums.album_path IN (SELECT photos.photo_path FROM photos WHERE photos.deleted_at IS NULL))"). Take(&cfg.Count) } c.Db(). Table("files"). Select("COUNT(*) AS files"). Where("file_missing = 0 AND file_root = ?", entity.RootOriginals). Take(&cfg.Count) c.Db(). Table("countries"). Select("(COUNT(*) - 1) AS countries"). Take(&cfg.Count) c.Db(). Table("places"). Select("SUM(photo_count > 0) AS places"). Where("id <> 'zz'"). Take(&cfg.Count) c.Db(). Order("country_slug"). Find(&cfg.Countries) // People are subjects with type person. cfg.Count.People, _ = query.PeopleCount() cfg.People, _ = query.People() c.Db(). Where("id IN (SELECT photos.camera_id FROM photos WHERE photos.photo_quality > -1 OR photos.deleted_at IS NULL)"). Where("deleted_at IS NULL"). Limit(10000).Order("camera_slug"). Find(&cfg.Cameras) c.Db(). Where("deleted_at IS NULL"). Limit(10000).Order("lens_slug"). Find(&cfg.Lenses) c.Db(). Where("deleted_at IS NULL AND album_favorite = 1"). Limit(20).Order("album_title"). Find(&cfg.Albums) c.Db(). Table("photos"). Where("photo_year > 0 AND (photos.photo_quality > -1 OR photos.deleted_at IS NULL)"). Order("photo_year DESC"). Pluck("DISTINCT photo_year", &cfg.Years) c.Db(). Table("categories"). Select("l.label_uid, l.custom_slug, l.label_name"). Joins("JOIN labels l ON categories.category_id = l.id"). Where("l.deleted_at IS NULL"). Group("l.custom_slug"). Order("l.custom_slug"). Limit(1000).Offset(0). Scan(&cfg.Categories) c.Db(). Table("albums"). Select("album_category"). Where("deleted_at IS NULL AND album_category <> ''"). Group("album_category"). Order("album_category"). Limit(1000).Offset(0). Pluck("album_category", &cfg.AlbumCategories) return cfg } // ClientRole provides the client config values for the specified user role. func (c *Config) ClientRole(role acl.Role) ClientConfig { return c.ClientUser(true).ApplyACL(acl.Resources, role) } // ClientSession provides the client config values for the specified session. func (c *Config) ClientSession(sess *entity.Session) ClientConfig { result := c.ClientUser(false).ApplyACL(acl.Resources, sess.User().AclRole()) result.Settings = c.SessionSettings(sess) return result }