photoprism/internal/config/client_config.go
Michael Mayer d8e0364dbb Search: Ignore public album filter if "Private" feat is disabled #2570
This needs to be very well tested and discussed, as these changes can
lead to private photos being accidentally published. Thank you!

Signed-off-by: Michael Mayer <michael@photoprism.app>
2022-08-01 15:57:19 +02:00

542 lines
17 KiB
Go

package config
import (
"strings"
"time"
"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"
ClientGuest ClientType = "guest"
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"`
Settings Settings `json:"settings"`
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"`
Ext Values `json:"ext"`
}
// 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
}
// PublicConfig returns public client config options with as little information as possible.
func (c *Config) PublicConfig() ClientConfig {
if c.Public() {
return c.UserConfig()
}
assets := c.ClientAssets()
settings := c.Settings()
result := ClientConfig{
Settings: Settings{
UI: settings.UI,
Search: settings.Search,
Maps: settings.Maps,
Features: settings.Features,
Share: settings.Share,
},
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: "public",
Name: c.Name(),
Edition: c.Edition(),
BaseUri: c.BaseUri(""),
StaticUri: c.StaticUri(),
CssUri: assets.AppCssUri(),
JsUri: assets.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(),
Status: "",
MapKey: "",
Thumbs: Thumbs,
Colors: colors.All.List(),
ManifestUri: c.ClientManifestUri(),
Clip: txt.ClipDefault,
PreviewToken: "public",
DownloadToken: "public",
Ext: ClientExt(c, ClientPublic),
}
return result
}
// GuestConfig returns client config options for the sharing with guests.
func (c *Config) GuestConfig() ClientConfig {
assets := c.ClientAssets()
settings := c.Settings()
result := ClientConfig{
Settings: Settings{
UI: settings.UI,
Search: settings.Search,
Maps: settings.Maps,
Features: settings.Features,
Share: settings.Share,
},
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: "guest",
Name: c.Name(),
Edition: c.Edition(),
BaseUri: c.BaseUri(""),
StaticUri: c.StaticUri(),
CssUri: assets.ShareCssUri(),
JsUri: assets.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: true,
UploadNSFW: c.UploadNSFW(),
Public: true,
Experimental: false,
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, ClientGuest),
}
return result
}
// UserConfig returns client configuration options for registered users.
func (c *Config) UserConfig() ClientConfig {
assets := c.ClientAssets()
result := ClientConfig{
Settings: *c.Settings(),
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: "user",
Name: c.Name(),
Edition: c.Edition(),
BaseUri: c.BaseUri(""),
StaticUri: c.StaticUri(),
CssUri: assets.AppCssUri(),
JsUri: assets.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(),
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),
}
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(&result.Pos)
c.Db().
Table("cameras").
Where("camera_slug <> 'zz' AND camera_slug <> ''").
Select("COUNT(*) AS cameras").
Take(&result.Count)
c.Db().
Table("lenses").
Where("lens_slug <> 'zz' AND lens_slug <> ''").
Select("COUNT(*) AS lenses").
Take(&result.Count)
if c.Settings().Features.Private {
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(&result.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(&result.Count)
}
result.Count.All = result.Count.Photos + result.Count.Live + result.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(&result.Count)
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(&result.Count)
c.Db().
Table("files").
Select("COUNT(*) AS files").
Where("file_missing = 0 AND file_root = ?", entity.RootOriginals).
Take(&result.Count)
c.Db().
Table("countries").
Select("(COUNT(*) - 1) AS countries").
Take(&result.Count)
c.Db().
Table("places").
Select("SUM(photo_count > 0) AS places").
Where("id <> 'zz'").
Take(&result.Count)
c.Db().
Order("country_slug").
Find(&result.Countries)
// People are subjects with type person.
result.Count.People, _ = query.PeopleCount()
result.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(&result.Cameras)
c.Db().
Where("deleted_at IS NULL").
Limit(10000).Order("lens_slug").
Find(&result.Lenses)
c.Db().
Where("deleted_at IS NULL AND album_favorite = 1").
Limit(20).Order("album_title").
Find(&result.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", &result.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(&result.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", &result.AlbumCategories)
return result
}