code.gitea.io/gitea@v1.21.7/routers/web/user/setting/profile.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2018 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package setting 6 7 import ( 8 "errors" 9 "fmt" 10 "io" 11 "math/big" 12 "net/http" 13 "os" 14 "path/filepath" 15 "strings" 16 17 "code.gitea.io/gitea/models/db" 18 "code.gitea.io/gitea/models/organization" 19 repo_model "code.gitea.io/gitea/models/repo" 20 user_model "code.gitea.io/gitea/models/user" 21 "code.gitea.io/gitea/modules/base" 22 "code.gitea.io/gitea/modules/context" 23 "code.gitea.io/gitea/modules/log" 24 "code.gitea.io/gitea/modules/setting" 25 "code.gitea.io/gitea/modules/translation" 26 "code.gitea.io/gitea/modules/typesniffer" 27 "code.gitea.io/gitea/modules/util" 28 "code.gitea.io/gitea/modules/web" 29 "code.gitea.io/gitea/modules/web/middleware" 30 "code.gitea.io/gitea/services/forms" 31 user_service "code.gitea.io/gitea/services/user" 32 ) 33 34 const ( 35 tplSettingsProfile base.TplName = "user/settings/profile" 36 tplSettingsAppearance base.TplName = "user/settings/appearance" 37 tplSettingsOrganization base.TplName = "user/settings/organization" 38 tplSettingsRepositories base.TplName = "user/settings/repos" 39 ) 40 41 // Profile render user's profile page 42 func Profile(ctx *context.Context) { 43 ctx.Data["Title"] = ctx.Tr("settings.profile") 44 ctx.Data["PageIsSettingsProfile"] = true 45 ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() 46 ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) 47 48 ctx.HTML(http.StatusOK, tplSettingsProfile) 49 } 50 51 // HandleUsernameChange handle username changes from user settings and admin interface 52 func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName string) error { 53 oldName := user.Name 54 // rename user 55 if err := user_service.RenameUser(ctx, user, newName); err != nil { 56 switch { 57 // Noop as username is not changed 58 case user_model.IsErrUsernameNotChanged(err): 59 ctx.Flash.Error(ctx.Tr("form.username_has_not_been_changed")) 60 // Non-local users are not allowed to change their username. 61 case user_model.IsErrUserIsNotLocal(err): 62 ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user")) 63 case user_model.IsErrUserAlreadyExist(err): 64 ctx.Flash.Error(ctx.Tr("form.username_been_taken")) 65 case user_model.IsErrEmailAlreadyUsed(err): 66 ctx.Flash.Error(ctx.Tr("form.email_been_used")) 67 case db.IsErrNameReserved(err): 68 ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName)) 69 case db.IsErrNamePatternNotAllowed(err): 70 ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) 71 case db.IsErrNameCharsNotAllowed(err): 72 ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName)) 73 default: 74 ctx.ServerError("ChangeUserName", err) 75 } 76 return err 77 } 78 log.Trace("User name changed: %s -> %s", oldName, newName) 79 return nil 80 } 81 82 // ProfilePost response for change user's profile 83 func ProfilePost(ctx *context.Context) { 84 form := web.GetForm(ctx).(*forms.UpdateProfileForm) 85 ctx.Data["Title"] = ctx.Tr("settings") 86 ctx.Data["PageIsSettingsProfile"] = true 87 ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() 88 ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) 89 90 if ctx.HasError() { 91 ctx.HTML(http.StatusOK, tplSettingsProfile) 92 return 93 } 94 95 if len(form.Name) != 0 && ctx.Doer.Name != form.Name { 96 log.Debug("Changing name for %s to %s", ctx.Doer.Name, form.Name) 97 if err := HandleUsernameChange(ctx, ctx.Doer, form.Name); err != nil { 98 ctx.Redirect(setting.AppSubURL + "/user/settings") 99 return 100 } 101 ctx.Doer.Name = form.Name 102 ctx.Doer.LowerName = strings.ToLower(form.Name) 103 } 104 105 ctx.Doer.FullName = form.FullName 106 ctx.Doer.KeepEmailPrivate = form.KeepEmailPrivate 107 ctx.Doer.Website = form.Website 108 ctx.Doer.Location = form.Location 109 ctx.Doer.Description = form.Description 110 ctx.Doer.KeepActivityPrivate = form.KeepActivityPrivate 111 ctx.Doer.Visibility = form.Visibility 112 if err := user_model.UpdateUserSetting(ctx, ctx.Doer); err != nil { 113 if _, ok := err.(user_model.ErrEmailAlreadyUsed); ok { 114 ctx.Flash.Error(ctx.Tr("form.email_been_used")) 115 ctx.Redirect(setting.AppSubURL + "/user/settings") 116 return 117 } 118 ctx.ServerError("UpdateUser", err) 119 return 120 } 121 122 log.Trace("User settings updated: %s", ctx.Doer.Name) 123 ctx.Flash.Success(ctx.Tr("settings.update_profile_success")) 124 ctx.Redirect(setting.AppSubURL + "/user/settings") 125 } 126 127 // UpdateAvatarSetting update user's avatar 128 // FIXME: limit size. 129 func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *user_model.User) error { 130 ctxUser.UseCustomAvatar = form.Source == forms.AvatarLocal 131 if len(form.Gravatar) > 0 { 132 if form.Avatar != nil { 133 ctxUser.Avatar = base.EncodeMD5(form.Gravatar) 134 } else { 135 ctxUser.Avatar = "" 136 } 137 ctxUser.AvatarEmail = form.Gravatar 138 } 139 140 if form.Avatar != nil && form.Avatar.Filename != "" { 141 fr, err := form.Avatar.Open() 142 if err != nil { 143 return fmt.Errorf("Avatar.Open: %w", err) 144 } 145 defer fr.Close() 146 147 if form.Avatar.Size > setting.Avatar.MaxFileSize { 148 return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) 149 } 150 151 data, err := io.ReadAll(fr) 152 if err != nil { 153 return fmt.Errorf("io.ReadAll: %w", err) 154 } 155 156 st := typesniffer.DetectContentType(data) 157 if !(st.IsImage() && !st.IsSvgImage()) { 158 return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) 159 } 160 if err = user_service.UploadAvatar(ctxUser, data); err != nil { 161 return fmt.Errorf("UploadAvatar: %w", err) 162 } 163 } else if ctxUser.UseCustomAvatar && ctxUser.Avatar == "" { 164 // No avatar is uploaded but setting has been changed to enable, 165 // generate a random one when needed. 166 if err := user_model.GenerateRandomAvatar(ctx, ctxUser); err != nil { 167 log.Error("GenerateRandomAvatar[%d]: %v", ctxUser.ID, err) 168 } 169 } 170 171 if err := user_model.UpdateUserCols(ctx, ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil { 172 return fmt.Errorf("UpdateUser: %w", err) 173 } 174 175 return nil 176 } 177 178 // AvatarPost response for change user's avatar request 179 func AvatarPost(ctx *context.Context) { 180 form := web.GetForm(ctx).(*forms.AvatarForm) 181 if err := UpdateAvatarSetting(ctx, form, ctx.Doer); err != nil { 182 ctx.Flash.Error(err.Error()) 183 } else { 184 ctx.Flash.Success(ctx.Tr("settings.update_avatar_success")) 185 } 186 187 ctx.Redirect(setting.AppSubURL + "/user/settings") 188 } 189 190 // DeleteAvatar render delete avatar page 191 func DeleteAvatar(ctx *context.Context) { 192 if err := user_service.DeleteAvatar(ctx.Doer); err != nil { 193 ctx.Flash.Error(err.Error()) 194 } 195 196 ctx.JSONRedirect(setting.AppSubURL + "/user/settings") 197 } 198 199 // Organization render all the organization of the user 200 func Organization(ctx *context.Context) { 201 ctx.Data["Title"] = ctx.Tr("settings.organization") 202 ctx.Data["PageIsSettingsOrganization"] = true 203 204 opts := organization.FindOrgOptions{ 205 ListOptions: db.ListOptions{ 206 PageSize: setting.UI.Admin.UserPagingNum, 207 Page: ctx.FormInt("page"), 208 }, 209 UserID: ctx.Doer.ID, 210 IncludePrivate: ctx.IsSigned, 211 } 212 213 if opts.Page <= 0 { 214 opts.Page = 1 215 } 216 217 orgs, err := organization.FindOrgs(opts) 218 if err != nil { 219 ctx.ServerError("FindOrgs", err) 220 return 221 } 222 total, err := organization.CountOrgs(opts) 223 if err != nil { 224 ctx.ServerError("CountOrgs", err) 225 return 226 } 227 ctx.Data["Orgs"] = orgs 228 pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) 229 pager.SetDefaultParams(ctx) 230 ctx.Data["Page"] = pager 231 ctx.HTML(http.StatusOK, tplSettingsOrganization) 232 } 233 234 // Repos display a list of all repositories of the user 235 func Repos(ctx *context.Context) { 236 ctx.Data["Title"] = ctx.Tr("settings.repos") 237 ctx.Data["PageIsSettingsRepos"] = true 238 ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories 239 ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories 240 241 opts := db.ListOptions{ 242 PageSize: setting.UI.Admin.UserPagingNum, 243 Page: ctx.FormInt("page"), 244 } 245 246 if opts.Page <= 0 { 247 opts.Page = 1 248 } 249 start := (opts.Page - 1) * opts.PageSize 250 end := start + opts.PageSize 251 252 adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories) 253 254 ctxUser := ctx.Doer 255 count := 0 256 257 if adoptOrDelete { 258 repoNames := make([]string, 0, setting.UI.Admin.UserPagingNum) 259 repos := map[string]*repo_model.Repository{} 260 // We're going to iterate by pagesize. 261 root := user_model.UserPath(ctxUser.Name) 262 if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { 263 if err != nil { 264 if os.IsNotExist(err) { 265 return nil 266 } 267 return err 268 } 269 if !d.IsDir() || path == root { 270 return nil 271 } 272 name := d.Name() 273 if !strings.HasSuffix(name, ".git") { 274 return filepath.SkipDir 275 } 276 name = name[:len(name)-4] 277 if repo_model.IsUsableRepoName(name) != nil || strings.ToLower(name) != name { 278 return filepath.SkipDir 279 } 280 if count >= start && count < end { 281 repoNames = append(repoNames, name) 282 } 283 count++ 284 return filepath.SkipDir 285 }); err != nil { 286 ctx.ServerError("filepath.WalkDir", err) 287 return 288 } 289 290 userRepos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{ 291 Actor: ctxUser, 292 Private: true, 293 ListOptions: db.ListOptions{ 294 Page: 1, 295 PageSize: setting.UI.Admin.UserPagingNum, 296 }, 297 LowerNames: repoNames, 298 }) 299 if err != nil { 300 ctx.ServerError("GetUserRepositories", err) 301 return 302 } 303 for _, repo := range userRepos { 304 if repo.IsFork { 305 if err := repo.GetBaseRepo(ctx); err != nil { 306 ctx.ServerError("GetBaseRepo", err) 307 return 308 } 309 } 310 repos[repo.LowerName] = repo 311 } 312 ctx.Data["Dirs"] = repoNames 313 ctx.Data["ReposMap"] = repos 314 } else { 315 repos, count64, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) 316 if err != nil { 317 ctx.ServerError("GetUserRepositories", err) 318 return 319 } 320 count = int(count64) 321 322 for i := range repos { 323 if repos[i].IsFork { 324 if err := repos[i].GetBaseRepo(ctx); err != nil { 325 ctx.ServerError("GetBaseRepo", err) 326 return 327 } 328 } 329 } 330 331 ctx.Data["Repos"] = repos 332 } 333 ctx.Data["ContextUser"] = ctxUser 334 pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) 335 pager.SetDefaultParams(ctx) 336 ctx.Data["Page"] = pager 337 ctx.HTML(http.StatusOK, tplSettingsRepositories) 338 } 339 340 // Appearance render user's appearance settings 341 func Appearance(ctx *context.Context) { 342 ctx.Data["Title"] = ctx.Tr("settings.appearance") 343 ctx.Data["PageIsSettingsAppearance"] = true 344 345 var hiddenCommentTypes *big.Int 346 val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) 347 if err != nil { 348 ctx.ServerError("GetUserSetting", err) 349 return 350 } 351 hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here 352 353 ctx.Data["IsCommentTypeGroupChecked"] = func(commentTypeGroup string) bool { 354 return forms.IsUserHiddenCommentTypeGroupChecked(commentTypeGroup, hiddenCommentTypes) 355 } 356 357 ctx.HTML(http.StatusOK, tplSettingsAppearance) 358 } 359 360 // UpdateUIThemePost is used to update users' specific theme 361 func UpdateUIThemePost(ctx *context.Context) { 362 form := web.GetForm(ctx).(*forms.UpdateThemeForm) 363 ctx.Data["Title"] = ctx.Tr("settings") 364 ctx.Data["PageIsSettingsAppearance"] = true 365 366 if ctx.HasError() { 367 ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") 368 return 369 } 370 371 if !form.IsThemeExists() { 372 ctx.Flash.Error(ctx.Tr("settings.theme_update_error")) 373 ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") 374 return 375 } 376 377 if err := user_model.UpdateUserTheme(ctx, ctx.Doer, form.Theme); err != nil { 378 ctx.Flash.Error(ctx.Tr("settings.theme_update_error")) 379 ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") 380 return 381 } 382 383 log.Trace("Update user theme: %s", ctx.Doer.Name) 384 ctx.Flash.Success(ctx.Tr("settings.theme_update_success")) 385 ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") 386 } 387 388 // UpdateUserLang update a user's language 389 func UpdateUserLang(ctx *context.Context) { 390 form := web.GetForm(ctx).(*forms.UpdateLanguageForm) 391 ctx.Data["Title"] = ctx.Tr("settings") 392 ctx.Data["PageIsSettingsAppearance"] = true 393 394 if len(form.Language) != 0 { 395 if !util.SliceContainsString(setting.Langs, form.Language) { 396 ctx.Flash.Error(ctx.Tr("settings.update_language_not_found", form.Language)) 397 ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") 398 return 399 } 400 ctx.Doer.Language = form.Language 401 } 402 403 if err := user_model.UpdateUserSetting(ctx, ctx.Doer); err != nil { 404 ctx.ServerError("UpdateUserSetting", err) 405 return 406 } 407 408 // Update the language to the one we just set 409 middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0) 410 411 log.Trace("User settings updated: %s", ctx.Doer.Name) 412 ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).Tr("settings.update_language_success")) 413 ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") 414 } 415 416 // UpdateUserHiddenComments update a user's shown comment types 417 func UpdateUserHiddenComments(ctx *context.Context) { 418 err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes, forms.UserHiddenCommentTypesFromRequest(ctx).String()) 419 if err != nil { 420 ctx.ServerError("SetUserSetting", err) 421 return 422 } 423 424 log.Trace("User settings updated: %s", ctx.Doer.Name) 425 ctx.Flash.Success(ctx.Tr("settings.saved_successfully")) 426 ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") 427 }