code.gitea.io/gitea@v1.21.7/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/context"
    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/setting"
    23  	"code.gitea.io/gitea/modules/util"
    24  	"code.gitea.io/gitea/routers/web/feed"
    25  	"code.gitea.io/gitea/routers/web/org"
    26  	shared_user "code.gitea.io/gitea/routers/web/shared/user"
    27  )
    28  
    29  // OwnerProfile render profile page for a user or a organization (aka, repo owner)
    30  func OwnerProfile(ctx *context.Context) {
    31  	if strings.Contains(ctx.Req.Header.Get("Accept"), "application/rss+xml") {
    32  		feed.ShowUserFeedRSS(ctx)
    33  		return
    34  	}
    35  	if strings.Contains(ctx.Req.Header.Get("Accept"), "application/atom+xml") {
    36  		feed.ShowUserFeedAtom(ctx)
    37  		return
    38  	}
    39  
    40  	if ctx.ContextUser.IsOrganization() {
    41  		org.Home(ctx)
    42  	} else {
    43  		userProfile(ctx)
    44  	}
    45  }
    46  
    47  func userProfile(ctx *context.Context) {
    48  	// check view permissions
    49  	if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
    50  		ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
    51  		return
    52  	}
    53  
    54  	ctx.Data["Title"] = ctx.ContextUser.DisplayName()
    55  	ctx.Data["PageIsUserProfile"] = true
    56  
    57  	// prepare heatmap data
    58  	if setting.Service.EnableUserHeatmap {
    59  		data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
    60  		if err != nil {
    61  			ctx.ServerError("GetUserHeatmapDataByUser", err)
    62  			return
    63  		}
    64  		ctx.Data["HeatmapData"] = data
    65  		ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
    66  	}
    67  
    68  	profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx)
    69  	defer profileClose()
    70  
    71  	showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
    72  	prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileGitRepo, profileReadmeBlob)
    73  	// call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing
    74  	shared_user.PrepareContextForProfileBigAvatar(ctx)
    75  	ctx.HTML(http.StatusOK, tplProfile)
    76  }
    77  
    78  func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadme *git.Blob) {
    79  	// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
    80  	// if there is not a profile readme, the overview tab should be treated as the repositories tab
    81  	tab := ctx.FormString("tab")
    82  	if tab == "" || tab == "overview" {
    83  		if profileReadme != nil {
    84  			tab = "overview"
    85  		} else {
    86  			tab = "repositories"
    87  		}
    88  	}
    89  	ctx.Data["TabName"] = tab
    90  	ctx.Data["HasProfileReadme"] = profileReadme != nil
    91  
    92  	page := ctx.FormInt("page")
    93  	if page <= 0 {
    94  		page = 1
    95  	}
    96  
    97  	pagingNum := setting.UI.User.RepoPagingNum
    98  	topicOnly := ctx.FormBool("topic")
    99  	var (
   100  		repos   []*repo_model.Repository
   101  		count   int64
   102  		total   int
   103  		orderBy db.SearchOrderBy
   104  	)
   105  
   106  	ctx.Data["SortType"] = ctx.FormString("sort")
   107  	switch ctx.FormString("sort") {
   108  	case "newest":
   109  		orderBy = db.SearchOrderByNewest
   110  	case "oldest":
   111  		orderBy = db.SearchOrderByOldest
   112  	case "recentupdate":
   113  		orderBy = db.SearchOrderByRecentUpdated
   114  	case "leastupdate":
   115  		orderBy = db.SearchOrderByLeastUpdated
   116  	case "reversealphabetically":
   117  		orderBy = db.SearchOrderByAlphabeticallyReverse
   118  	case "alphabetically":
   119  		orderBy = db.SearchOrderByAlphabetically
   120  	case "moststars":
   121  		orderBy = db.SearchOrderByStarsReverse
   122  	case "feweststars":
   123  		orderBy = db.SearchOrderByStars
   124  	case "mostforks":
   125  		orderBy = db.SearchOrderByForksReverse
   126  	case "fewestforks":
   127  		orderBy = db.SearchOrderByForks
   128  	default:
   129  		ctx.Data["SortType"] = "recentupdate"
   130  		orderBy = db.SearchOrderByRecentUpdated
   131  	}
   132  
   133  	keyword := ctx.FormTrim("q")
   134  	ctx.Data["Keyword"] = keyword
   135  
   136  	language := ctx.FormTrim("language")
   137  	ctx.Data["Language"] = language
   138  
   139  	followers, numFollowers, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{
   140  		PageSize: pagingNum,
   141  		Page:     page,
   142  	})
   143  	if err != nil {
   144  		ctx.ServerError("GetUserFollowers", err)
   145  		return
   146  	}
   147  	ctx.Data["NumFollowers"] = numFollowers
   148  	following, numFollowing, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{
   149  		PageSize: pagingNum,
   150  		Page:     page,
   151  	})
   152  	if err != nil {
   153  		ctx.ServerError("GetUserFollowing", err)
   154  		return
   155  	}
   156  	ctx.Data["NumFollowing"] = numFollowing
   157  
   158  	switch tab {
   159  	case "followers":
   160  		ctx.Data["Cards"] = followers
   161  		total = int(numFollowers)
   162  	case "following":
   163  		ctx.Data["Cards"] = following
   164  		total = int(numFollowing)
   165  	case "activity":
   166  		date := ctx.FormString("date")
   167  		pagingNum = setting.UI.FeedPagingNum
   168  		items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
   169  			RequestedUser:   ctx.ContextUser,
   170  			Actor:           ctx.Doer,
   171  			IncludePrivate:  showPrivate,
   172  			OnlyPerformedBy: true,
   173  			IncludeDeleted:  false,
   174  			Date:            date,
   175  			ListOptions: db.ListOptions{
   176  				PageSize: pagingNum,
   177  				Page:     page,
   178  			},
   179  		})
   180  		if err != nil {
   181  			ctx.ServerError("GetFeeds", err)
   182  			return
   183  		}
   184  		ctx.Data["Feeds"] = items
   185  		ctx.Data["Date"] = date
   186  
   187  		total = int(count)
   188  	case "stars":
   189  		ctx.Data["PageIsProfileStarList"] = true
   190  		repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
   191  			ListOptions: db.ListOptions{
   192  				PageSize: pagingNum,
   193  				Page:     page,
   194  			},
   195  			Actor:              ctx.Doer,
   196  			Keyword:            keyword,
   197  			OrderBy:            orderBy,
   198  			Private:            ctx.IsSigned,
   199  			StarredByID:        ctx.ContextUser.ID,
   200  			Collaborate:        util.OptionalBoolFalse,
   201  			TopicOnly:          topicOnly,
   202  			Language:           language,
   203  			IncludeDescription: setting.UI.SearchRepoDescription,
   204  		})
   205  		if err != nil {
   206  			ctx.ServerError("SearchRepository", err)
   207  			return
   208  		}
   209  
   210  		total = int(count)
   211  	case "watching":
   212  		repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
   213  			ListOptions: db.ListOptions{
   214  				PageSize: pagingNum,
   215  				Page:     page,
   216  			},
   217  			Actor:              ctx.Doer,
   218  			Keyword:            keyword,
   219  			OrderBy:            orderBy,
   220  			Private:            ctx.IsSigned,
   221  			WatchedByID:        ctx.ContextUser.ID,
   222  			Collaborate:        util.OptionalBoolFalse,
   223  			TopicOnly:          topicOnly,
   224  			Language:           language,
   225  			IncludeDescription: setting.UI.SearchRepoDescription,
   226  		})
   227  		if err != nil {
   228  			ctx.ServerError("SearchRepository", err)
   229  			return
   230  		}
   231  
   232  		total = int(count)
   233  	case "overview":
   234  		if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
   235  			log.Error("failed to GetBlobContent: %v", err)
   236  		} else {
   237  			if profileContent, err := markdown.RenderString(&markup.RenderContext{
   238  				Ctx:     ctx,
   239  				GitRepo: profileGitRepo,
   240  				Links: markup.Links{
   241  					// Give the repo link to the markdown render for the full link of media element.
   242  					// the media link usually be like /[user]/[repoName]/media/branch/[branchName],
   243  					// 	Eg. /Tom/.profile/media/branch/main
   244  					// The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
   245  					//	https://docs.gitea.com/usage/profile-readme
   246  					Base:       profileDbRepo.Link(),
   247  					BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
   248  				},
   249  				Metas: map[string]string{"mode": "document"},
   250  			}, bytes); err != nil {
   251  				log.Error("failed to RenderString: %v", err)
   252  			} else {
   253  				ctx.Data["ProfileReadme"] = profileContent
   254  			}
   255  		}
   256  	default: // default to "repositories"
   257  		repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
   258  			ListOptions: db.ListOptions{
   259  				PageSize: pagingNum,
   260  				Page:     page,
   261  			},
   262  			Actor:              ctx.Doer,
   263  			Keyword:            keyword,
   264  			OwnerID:            ctx.ContextUser.ID,
   265  			OrderBy:            orderBy,
   266  			Private:            ctx.IsSigned,
   267  			Collaborate:        util.OptionalBoolFalse,
   268  			TopicOnly:          topicOnly,
   269  			Language:           language,
   270  			IncludeDescription: setting.UI.SearchRepoDescription,
   271  		})
   272  		if err != nil {
   273  			ctx.ServerError("SearchRepository", err)
   274  			return
   275  		}
   276  
   277  		total = int(count)
   278  	}
   279  	ctx.Data["Repos"] = repos
   280  	ctx.Data["Total"] = total
   281  
   282  	err = shared_user.LoadHeaderCount(ctx)
   283  	if err != nil {
   284  		ctx.ServerError("LoadHeaderCount", err)
   285  		return
   286  	}
   287  
   288  	pager := context.NewPagination(total, pagingNum, page, 5)
   289  	pager.SetDefaultParams(ctx)
   290  	pager.AddParam(ctx, "tab", "TabName")
   291  	if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
   292  		pager.AddParam(ctx, "language", "Language")
   293  	}
   294  	if tab == "activity" {
   295  		pager.AddParam(ctx, "date", "Date")
   296  	}
   297  	ctx.Data["Page"] = pager
   298  }
   299  
   300  // Action response for follow/unfollow user request
   301  func Action(ctx *context.Context) {
   302  	var err error
   303  	switch ctx.FormString("action") {
   304  	case "follow":
   305  		err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
   306  	case "unfollow":
   307  		err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
   308  	}
   309  
   310  	if err != nil {
   311  		log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err)
   312  		ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action")))
   313  		return
   314  	}
   315  	ctx.JSONOK()
   316  }