diff --git a/cmd/web.go b/cmd/web.go index 4c4dbe94e94..1ce5acc783a 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -226,7 +226,8 @@ func runWeb(ctx *cli.Context) error { m.Group("/user/settings", func() { m.Get("", user.Settings) m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost) - m.Post("/avatar", binding.MultipartForm(auth.UploadAvatarForm{}), user.SettingsAvatar) + m.Combo("/avatar").Get(user.SettingsAvatar). + Post(binding.MultipartForm(auth.AvatarForm{}), user.SettingsAvatarPost) m.Post("/avatar/delete", user.SettingsDeleteAvatar) m.Combo("/email").Get(user.SettingsEmails). Post(bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost) @@ -375,7 +376,7 @@ func runWeb(ctx *cli.Context) error { m.Group("/settings", func() { m.Combo("").Get(org.Settings). Post(bindIgnErr(auth.UpdateOrgSettingForm{}), org.SettingsPost) - m.Post("/avatar", binding.MultipartForm(auth.UploadAvatarForm{}), org.SettingsAvatar) + m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), org.SettingsAvatar) m.Post("/avatar/delete", org.SettingsDeleteAvatar) m.Group("/hooks", func() { diff --git a/conf/app.ini b/conf/app.ini index 3c36195ff6b..e46436e65da 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -230,6 +230,9 @@ AVATAR_UPLOAD_PATH = data/avatars ; or a custom avatar source, like: http://cn.gravatar.com/avatar/ GRAVATAR_SOURCE = gravatar DISABLE_GRAVATAR = false +; Federated avatar lookup uses DNS to discover avatar associated +; with emails, see http://www.libravatar.org +ENABLE_FEDERATED_AVATAR = false [attachment] ; Whether attachments are enabled. Defaults to `true` diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index dd3d54d6f88..99b5d173638 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -96,6 +96,7 @@ offline_mode = Enable Offline Mode offline_mode_popup = Disable CDN even in production mode, all resource files will be served locally. disable_gravatar = Disable Gravatar Service disable_gravatar_popup = Disable Gravatar and custom sources, all avatars are uploaded by users or default. +federated_avatar_lookup = Enable Federated Avatars Lookup disable_registration = Disable Self-registration disable_registration_popup = Disable user self-registration, only admin can create accounts. enable_captcha = Enable Captcha @@ -239,6 +240,7 @@ form.name_pattern_not_allowed = Username pattern '%s' is not allowed. [settings] profile = Profile password = Password +avatar = Avatar ssh_keys = SSH Keys social = Social Accounts applications = Applications @@ -259,7 +261,9 @@ change_username_prompt = This change will affect the way how links relate to you continue = Continue cancel = Cancel -enable_custom_avatar = Enable Custom Avatar +lookup_avatar_by_mail = Lookup Avatar by mail +federated_avatar_lookup = Federated Avatar Lookup +enable_custom_avatar = Use Custom Avatar choose_new_avatar = Choose new avatar update_avatar = Update Avatar Setting delete_current_avatar = Delete Current Avatar @@ -1044,6 +1048,7 @@ config.cookie_life_time = Cookie Life Time config.picture_config = Picture Configuration config.picture_service = Picture Service config.disable_gravatar = Disable Gravatar +config.enable_federated_avatar = Enable Federated Avatars config.log_config = Log Configuration config.log_mode = Log Mode diff --git a/models/user.go b/models/user.go index 941dc054aa9..d08b1cc8fa4 100644 --- a/models/user.go +++ b/models/user.go @@ -254,7 +254,7 @@ func (u *User) RelAvatarLink() string { return setting.AppSubUrl + "/avatars/" + com.ToStr(u.ID) } - return setting.GravatarSource + u.Avatar + return base.AvatarLink(u.AvatarEmail) } // AvatarLink returns user avatar absolute link. diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index 57451d9eb6b..7bd6c7b9bc3 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -36,11 +36,12 @@ type InstallForm struct { RegisterConfirm bool MailNotify bool - OfflineMode bool - DisableGravatar bool - DisableRegistration bool - EnableCaptcha bool - RequireSignInView bool + OfflineMode bool + DisableGravatar bool + EnableFederatedAvatar bool + DisableRegistration bool + EnableCaptcha bool + RequireSignInView bool AdminName string `binding:"OmitEmpty;AlphaDashDot;MaxSize(30)" locale:"install.admin_name"` AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"` @@ -93,19 +94,25 @@ type UpdateProfileForm struct { Email string `binding:"Required;Email;MaxSize(254)"` Website string `binding:"Url;MaxSize(100)"` Location string `binding:"MaxSize(50)"` - Gravatar string `binding:"OmitEmpty;Email;MaxSize(254)"` } func (f *UpdateProfileForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { return validate(errs, ctx.Data, f, ctx.Locale) } -type UploadAvatarForm struct { - Enable bool - Avatar *multipart.FileHeader +const ( + AVATAR_LOCAL string = "local" + AVATAR_BYMAIL string = "bymail" +) + +type AvatarForm struct { + Source string + Avatar *multipart.FileHeader + Gravatar string `binding:"OmitEmpty;Email;MaxSize(254)"` + Federavatar bool } -func (f *UploadAvatarForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { +func (f *AvatarForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { return validate(errs, ctx.Data, f, ctx.Locale) } diff --git a/modules/base/tool.go b/modules/base/tool.go index f045cb2270a..b5125cab8a9 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -205,12 +205,26 @@ func HashEmail(email string) string { } // AvatarLink returns avatar link by given email. -func AvatarLink(email string) string { - if setting.DisableGravatar || setting.OfflineMode { - return setting.AppSubUrl + "/img/avatar_default.png" +func AvatarLink(email string) (url string) { + + if !setting.OfflineMode { + if setting.EnableFederatedAvatar && setting.LibravatarService != nil { + var err error + url, err = setting.LibravatarService.FromEmail(email) + if err != nil { + log.Error(1, "LibravatarService.FromEmail:: %v", err) + } + } + if len(url) == 0 && !setting.DisableGravatar { + url = setting.GravatarSource + HashEmail(email) + } + } + + if len(url) == 0 { + url = setting.AppSubUrl + "/img/avatar_default.png" } - return setting.GravatarSource + HashEmail(email) + return url } // Seconds-based time units diff --git a/modules/setting/setting.go b/modules/setting/setting.go index f59aa0884df..b8e8f65ef10 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -21,6 +21,7 @@ import ( "github.com/go-macaron/session" _ "github.com/go-macaron/session/redis" "gopkg.in/ini.v1" + "github.com/strk/go-libravatar" "github.com/gogits/gogs/modules/bindata" "github.com/gogits/gogs/modules/log" @@ -140,9 +141,11 @@ var ( } // Picture settings - AvatarUploadPath string - GravatarSource string - DisableGravatar bool + AvatarUploadPath string + GravatarSource string + DisableGravatar bool + EnableFederatedAvatar bool + LibravatarService *libravatar.Libravatar // Log settings LogRootPath string @@ -462,8 +465,24 @@ func NewContext() { GravatarSource = source } DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool() + EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool() if OfflineMode { DisableGravatar = true + EnableFederatedAvatar = false + } + + if !DisableGravatar && EnableFederatedAvatar { + LibravatarService = libravatar.New() + parts := strings.Split(GravatarSource, "/") + if len(parts) >= 3 { + if parts[0] == "https:" { + LibravatarService.SetUseHTTPS(true) + LibravatarService.SetSecureFallbackHost(parts[2]) + } else { + LibravatarService.SetUseHTTPS(false) + LibravatarService.SetFallbackHost(parts[2]) + } + } } if err = Cfg.Section("ui").MapTo(&UI); err != nil { diff --git a/routers/admin/admin.go b/routers/admin/admin.go index bc850b638ee..b8eb6743d7b 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -222,6 +222,7 @@ func Config(ctx *context.Context) { ctx.Data["SessionConfig"] = setting.SessionConfig ctx.Data["DisableGravatar"] = setting.DisableGravatar + ctx.Data["EnableFederatedAvatar"] = setting.EnableFederatedAvatar type logger struct { Mode, Config string diff --git a/routers/install.go b/routers/install.go index 0af1f446c0d..8b96ff82542 100644 --- a/routers/install.go +++ b/routers/install.go @@ -169,6 +169,7 @@ func Install(ctx *context.Context) { // Server and other services settings form.OfflineMode = setting.OfflineMode form.DisableGravatar = setting.DisableGravatar + form.EnableFederatedAvatar = setting.EnableFederatedAvatar form.DisableRegistration = setting.Service.DisableRegistration form.EnableCaptcha = setting.Service.EnableCaptcha form.RequireSignInView = setting.Service.RequireSignInView @@ -329,6 +330,7 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { cfg.Section("server").Key("OFFLINE_MODE").SetValue(com.ToStr(form.OfflineMode)) cfg.Section("picture").Key("DISABLE_GRAVATAR").SetValue(com.ToStr(form.DisableGravatar)) + cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(com.ToStr(form.EnableFederatedAvatar)) cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(com.ToStr(form.DisableRegistration)) cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(com.ToStr(form.EnableCaptcha)) cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(com.ToStr(form.RequireSignInView)) diff --git a/routers/org/setting.go b/routers/org/setting.go index 128fba36a85..14681e4c59c 100644 --- a/routers/org/setting.go +++ b/routers/org/setting.go @@ -83,8 +83,8 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) { ctx.Redirect(ctx.Org.OrgLink + "/settings") } -func SettingsAvatar(ctx *context.Context, form auth.UploadAvatarForm) { - form.Enable = true +func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) { + form.Source = auth.AVATAR_LOCAL if err := user.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil { ctx.Flash.Error(err.Error()) } else { diff --git a/routers/user/setting.go b/routers/user/setting.go index 7412ce34d13..c5441f3543a 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -22,6 +22,7 @@ import ( const ( SETTINGS_PROFILE base.TplName = "user/settings/profile" + SETTINGS_AVATAR base.TplName = "user/settings/avatar" SETTINGS_PASSWORD base.TplName = "user/settings/password" SETTINGS_EMAILS base.TplName = "user/settings/email" SETTINGS_SSH_KEYS base.TplName = "user/settings/sshkeys" @@ -91,10 +92,6 @@ func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) { ctx.User.Email = form.Email ctx.User.Website = form.Website ctx.User.Location = form.Location - if len(form.Gravatar) > 0 { - ctx.User.Avatar = base.EncodeMD5(form.Gravatar) - ctx.User.AvatarEmail = form.Gravatar - } if err := models.UpdateUser(ctx.User); err != nil { ctx.Handle(500, "UpdateUser", err) return @@ -106,8 +103,12 @@ func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) { } // FIXME: limit size. -func UpdateAvatarSetting(ctx *context.Context, form auth.UploadAvatarForm, ctxUser *models.User) error { - ctxUser.UseCustomAvatar = form.Enable +func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *models.User) error { + ctxUser.UseCustomAvatar = form.Source == auth.AVATAR_LOCAL + if len(form.Gravatar) > 0 { + ctxUser.Avatar = base.EncodeMD5(form.Gravatar) + ctxUser.AvatarEmail = form.Gravatar + } if form.Avatar != nil { fr, err := form.Avatar.Open() @@ -129,7 +130,7 @@ func UpdateAvatarSetting(ctx *context.Context, form auth.UploadAvatarForm, ctxUs } else { // No avatar is uploaded but setting has been changed to enable, // generate a random one when needed. - if form.Enable && !com.IsFile(ctxUser.CustomAvatarPath()) { + if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) { if err := ctxUser.GenerateRandomAvatar(); err != nil { log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err) } @@ -143,14 +144,20 @@ func UpdateAvatarSetting(ctx *context.Context, form auth.UploadAvatarForm, ctxUs return nil } -func SettingsAvatar(ctx *context.Context, form auth.UploadAvatarForm) { +func SettingsAvatar(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAvatar"] = true + ctx.HTML(200, SETTINGS_AVATAR) +} + +func SettingsAvatarPost(ctx *context.Context, form auth.AvatarForm) { if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil { ctx.Flash.Error(err.Error()) } else { ctx.Flash.Success(ctx.Tr("settings.update_avatar_success")) } - ctx.Redirect(setting.AppSubUrl + "/user/settings") + ctx.Redirect(setting.AppSubUrl + "/user/settings/avatar") } func SettingsDeleteAvatar(ctx *context.Context) { @@ -158,7 +165,7 @@ func SettingsDeleteAvatar(ctx *context.Context) { ctx.Flash.Error(err.Error()) } - ctx.Redirect(setting.AppSubUrl + "/user/settings") + ctx.Redirect(setting.AppSubUrl + "/user/settings/avatar") } func SettingsPassword(ctx *context.Context) { diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 880fca713ba..93fc404ba10 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -215,6 +215,12 @@