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  }