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