code.gitea.io/gitea@v1.22.3/routers/web/user/profile.go (about)

     1  // Copyright 2015 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package user
     6  
     7  import (
     8  	"fmt"
     9  	"net/http"
    10  	"path"
    11  	"strings"
    12  
    13  	activities_model "code.gitea.io/gitea/models/activities"
    14  	"code.gitea.io/gitea/models/db"
    15  	repo_model "code.gitea.io/gitea/models/repo"
    16  	user_model "code.gitea.io/gitea/models/user"
    17  	"code.gitea.io/gitea/modules/base"
    18  	"code.gitea.io/gitea/modules/git"
    19  	"code.gitea.io/gitea/modules/log"
    20  	"code.gitea.io/gitea/modules/markup"
    21  	"code.gitea.io/gitea/modules/markup/markdown"
    22  	"code.gitea.io/gitea/modules/optional"
    23  	"code.gitea.io/gitea/modules/setting"
    24  	"code.gitea.io/gitea/modules/util"
    25  	"code.gitea.io/gitea/routers/web/feed"
    26  	"code.gitea.io/gitea/routers/web/org"
    27  	shared_user "code.gitea.io/gitea/routers/web/shared/user"
    28  	"code.gitea.io/gitea/services/context"
    29  )
    30  
    31  const (
    32  	tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar"
    33  	tplFollowUnfollow   base.TplName = "org/follow_unfollow"
    34  )
    35  
    36  // OwnerProfile render profile page for a user or a organization (aka, repo owner)
    37  func OwnerProfile(ctx *context.Context) {
    38  	if strings.Contains(ctx.Req.Header.Get("Accept"), "application/rss+xml") {
    39  		feed.ShowUserFeedRSS(ctx)
    40  		return
    41  	}
    42  	if strings.Contains(ctx.Req.Header.Get("Accept"), "application/atom+xml") {
    43  		feed.ShowUserFeedAtom(ctx)
    44  		return
    45  	}
    46  
    47  	if ctx.ContextUser.IsOrganization() {
    48  		org.Home(ctx)
    49  	} else {
    50  		userProfile(ctx)
    51  	}
    52  }
    53  
    54  func userProfile(ctx *context.Context) {
    55  	// check view permissions
    56  	if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
    57  		ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
    58  		return
    59  	}
    60  
    61  	ctx.Data["Title"] = ctx.ContextUser.DisplayName()
    62  	ctx.Data["PageIsUserProfile"] = true
    63  
    64  	// prepare heatmap data
    65  	if setting.Service.EnableUserHeatmap {
    66  		data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
    67  		if err != nil {
    68  			ctx.ServerError("GetUserHeatmapDataByUser", err)
    69  			return
    70  		}
    71  		ctx.Data["HeatmapData"] = data
    72  		ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
    73  	}
    74  
    75  	profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
    76  	defer profileClose()
    77  
    78  	showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
    79  	prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileGitRepo, profileReadmeBlob)
    80  	// call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing
    81  	shared_user.PrepareContextForProfileBigAvatar(ctx)
    82  	ctx.HTML(http.StatusOK, tplProfile)
    83  }
    84  
    85  func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadme *git.Blob) {
    86  	// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
    87  	// if there is not a profile readme, the overview tab should be treated as the repositories tab
    88  	tab := ctx.FormString("tab")
    89  	if tab == "" || tab == "overview" {
    90  		if profileReadme != nil {
    91  			tab = "overview"
    92  		} else {
    93  			tab = "repositories"
    94  		}
    95  	}
    96  	ctx.Data["TabName"] = tab
    97  	ctx.Data["HasProfileReadme"] = profileReadme != nil
    98  
    99  	page := ctx.FormInt("page")
   100  	if page <= 0 {
   101  		page = 1
   102  	}
   103  
   104  	pagingNum := setting.UI.User.RepoPagingNum
   105  	topicOnly := ctx.FormBool("topic")
   106  	var (
   107  		repos   []*repo_model.Repository
   108  		count   int64
   109  		total   int
   110  		orderBy db.SearchOrderBy
   111  	)
   112  
   113  	ctx.Data["SortType"] = ctx.FormString("sort")
   114  	switch ctx.FormString("sort") {
   115  	case "newest":
   116  		orderBy = db.SearchOrderByNewest
   117  	case "oldest":
   118  		orderBy = db.SearchOrderByOldest
   119  	case "recentupdate":
   120  		orderBy = db.SearchOrderByRecentUpdated
   121  	case "leastupdate":
   122  		orderBy = db.SearchOrderByLeastUpdated
   123  	case "reversealphabetically":
   124  		orderBy = db.SearchOrderByAlphabeticallyReverse
   125  	case "alphabetically":
   126  		orderBy = db.SearchOrderByAlphabetically
   127  	case "moststars":
   128  		orderBy = db.SearchOrderByStarsReverse
   129  	case "feweststars":
   130  		orderBy = db.SearchOrderByStars
   131  	case "mostforks":
   132  		orderBy = db.SearchOrderByForksReverse
   133  	case "fewestforks":
   134  		orderBy = db.SearchOrderByForks
   135  	case "size":
   136  		orderBy = db.SearchOrderByGitSize
   137  	case "reversesize":
   138  		orderBy = db.SearchOrderByGitSizeReverse
   139  	default:
   140  		ctx.Data["SortType"] = "recentupdate"
   141  		orderBy = db.SearchOrderByRecentUpdated
   142  	}
   143  
   144  	keyword := ctx.FormTrim("q")
   145  	ctx.Data["Keyword"] = keyword
   146  
   147  	language := ctx.FormTrim("language")
   148  	ctx.Data["Language"] = language
   149  
   150  	followers, numFollowers, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{
   151  		PageSize: pagingNum,
   152  		Page:     page,
   153  	})
   154  	if err != nil {
   155  		ctx.ServerError("GetUserFollowers", err)
   156  		return
   157  	}
   158  	ctx.Data["NumFollowers"] = numFollowers
   159  	following, numFollowing, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{
   160  		PageSize: pagingNum,
   161  		Page:     page,
   162  	})
   163  	if err != nil {
   164  		ctx.ServerError("GetUserFollowing", err)
   165  		return
   166  	}
   167  	ctx.Data["NumFollowing"] = numFollowing
   168  
   169  	archived := ctx.FormOptionalBool("archived")
   170  	ctx.Data["IsArchived"] = archived
   171  
   172  	fork := ctx.FormOptionalBool("fork")
   173  	ctx.Data["IsFork"] = fork
   174  
   175  	mirror := ctx.FormOptionalBool("mirror")
   176  	ctx.Data["IsMirror"] = mirror
   177  
   178  	template := ctx.FormOptionalBool("template")
   179  	ctx.Data["IsTemplate"] = template
   180  
   181  	private := ctx.FormOptionalBool("private")
   182  	ctx.Data["IsPrivate"] = private
   183  
   184  	switch tab {
   185  	case "followers":
   186  		ctx.Data["Cards"] = followers
   187  		total = int(numFollowers)
   188  	case "following":
   189  		ctx.Data["Cards"] = following
   190  		total = int(numFollowing)
   191  	case "activity":
   192  		date := ctx.FormString("date")
   193  		pagingNum = setting.UI.FeedPagingNum
   194  		items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
   195  			RequestedUser:   ctx.ContextUser,
   196  			Actor:           ctx.Doer,
   197  			IncludePrivate:  showPrivate,
   198  			OnlyPerformedBy: true,
   199  			IncludeDeleted:  false,
   200  			Date:            date,
   201  			ListOptions: db.ListOptions{
   202  				PageSize: pagingNum,
   203  				Page:     page,
   204  			},
   205  		})
   206  		if err != nil {
   207  			ctx.ServerError("GetFeeds", err)
   208  			return
   209  		}
   210  		ctx.Data["Feeds"] = items
   211  		ctx.Data["Date"] = date
   212  
   213  		total = int(count)
   214  	case "stars":
   215  		ctx.Data["PageIsProfileStarList"] = true
   216  		repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
   217  			ListOptions: db.ListOptions{
   218  				PageSize: pagingNum,
   219  				Page:     page,
   220  			},
   221  			Actor:              ctx.Doer,
   222  			Keyword:            keyword,
   223  			OrderBy:            orderBy,
   224  			Private:            ctx.IsSigned,
   225  			StarredByID:        ctx.ContextUser.ID,
   226  			Collaborate:        optional.Some(false),
   227  			TopicOnly:          topicOnly,
   228  			Language:           language,
   229  			IncludeDescription: setting.UI.SearchRepoDescription,
   230  			Archived:           archived,
   231  			Fork:               fork,
   232  			Mirror:             mirror,
   233  			Template:           template,
   234  			IsPrivate:          private,
   235  		})
   236  		if err != nil {
   237  			ctx.ServerError("SearchRepository", err)
   238  			return
   239  		}
   240  
   241  		total = int(count)
   242  	case "watching":
   243  		repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
   244  			ListOptions: db.ListOptions{
   245  				PageSize: pagingNum,
   246  				Page:     page,
   247  			},
   248  			Actor:              ctx.Doer,
   249  			Keyword:            keyword,
   250  			OrderBy:            orderBy,
   251  			Private:            ctx.IsSigned,
   252  			WatchedByID:        ctx.ContextUser.ID,
   253  			Collaborate:        optional.Some(false),
   254  			TopicOnly:          topicOnly,
   255  			Language:           language,
   256  			IncludeDescription: setting.UI.SearchRepoDescription,
   257  			Archived:           archived,
   258  			Fork:               fork,
   259  			Mirror:             mirror,
   260  			Template:           template,
   261  			IsPrivate:          private,
   262  		})
   263  		if err != nil {
   264  			ctx.ServerError("SearchRepository", err)
   265  			return
   266  		}
   267  
   268  		total = int(count)
   269  	case "overview":
   270  		if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
   271  			log.Error("failed to GetBlobContent: %v", err)
   272  		} else {
   273  			if profileContent, err := markdown.RenderString(&markup.RenderContext{
   274  				Ctx:     ctx,
   275  				GitRepo: profileGitRepo,
   276  				Links: markup.Links{
   277  					// Give the repo link to the markdown render for the full link of media element.
   278  					// the media link usually be like /[user]/[repoName]/media/branch/[branchName],
   279  					// 	Eg. /Tom/.profile/media/branch/main
   280  					// The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
   281  					//	https://docs.gitea.com/usage/profile-readme
   282  					Base:       profileDbRepo.Link(),
   283  					BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
   284  				},
   285  				Metas: map[string]string{"mode": "document"},
   286  			}, bytes); err != nil {
   287  				log.Error("failed to RenderString: %v", err)
   288  			} else {
   289  				ctx.Data["ProfileReadme"] = profileContent
   290  			}
   291  		}
   292  	default: // default to "repositories"
   293  		repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
   294  			ListOptions: db.ListOptions{
   295  				PageSize: pagingNum,
   296  				Page:     page,
   297  			},
   298  			Actor:              ctx.Doer,
   299  			Keyword:            keyword,
   300  			OwnerID:            ctx.ContextUser.ID,
   301  			OrderBy:            orderBy,
   302  			Private:            ctx.IsSigned,
   303  			Collaborate:        optional.Some(false),
   304  			TopicOnly:          topicOnly,
   305  			Language:           language,
   306  			IncludeDescription: setting.UI.SearchRepoDescription,
   307  			Archived:           archived,
   308  			Fork:               fork,
   309  			Mirror:             mirror,
   310  			Template:           template,
   311  			IsPrivate:          private,
   312  		})
   313  		if err != nil {
   314  			ctx.ServerError("SearchRepository", err)
   315  			return
   316  		}
   317  
   318  		total = int(count)
   319  	}
   320  	ctx.Data["Repos"] = repos
   321  	ctx.Data["Total"] = total
   322  
   323  	err = shared_user.LoadHeaderCount(ctx)
   324  	if err != nil {
   325  		ctx.ServerError("LoadHeaderCount", err)
   326  		return
   327  	}
   328  
   329  	pager := context.NewPagination(total, pagingNum, page, 5)
   330  	pager.SetDefaultParams(ctx)
   331  	pager.AddParamString("tab", tab)
   332  	if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
   333  		pager.AddParamString("language", language)
   334  	}
   335  	if tab == "activity" {
   336  		if ctx.Data["Date"] != nil {
   337  			pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"]))
   338  		}
   339  	}
   340  	if archived.Has() {
   341  		pager.AddParamString("archived", fmt.Sprint(archived.Value()))
   342  	}
   343  	if fork.Has() {
   344  		pager.AddParamString("fork", fmt.Sprint(fork.Value()))
   345  	}
   346  	if mirror.Has() {
   347  		pager.AddParamString("mirror", fmt.Sprint(mirror.Value()))
   348  	}
   349  	if template.Has() {
   350  		pager.AddParamString("template", fmt.Sprint(template.Value()))
   351  	}
   352  	if private.Has() {
   353  		pager.AddParamString("private", fmt.Sprint(private.Value()))
   354  	}
   355  	ctx.Data["Page"] = pager
   356  }
   357  
   358  // Action response for follow/unfollow user request
   359  func Action(ctx *context.Context) {
   360  	var err error
   361  	switch ctx.FormString("action") {
   362  	case "follow":
   363  		err = user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser)
   364  	case "unfollow":
   365  		err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
   366  	}
   367  
   368  	if err != nil {
   369  		log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err)
   370  		ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
   371  		return
   372  	}
   373  
   374  	if ctx.ContextUser.IsIndividual() {
   375  		shared_user.PrepareContextForProfileBigAvatar(ctx)
   376  		ctx.HTML(http.StatusOK, tplProfileBigAvatar)
   377  		return
   378  	} else if ctx.ContextUser.IsOrganization() {
   379  		ctx.Data["Org"] = ctx.ContextUser
   380  		ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
   381  		ctx.HTML(http.StatusOK, tplFollowUnfollow)
   382  		return
   383  	}
   384  	log.Error("Failed to apply action %q: unsupport context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type)
   385  	ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
   386  }