Browse Source

templates: make state changing routes to POST method (#5541)

- pkg/context: add ParamsUser to unify the injection process
Unknwon 6 months ago
parent
commit
f545faa06d

+ 5 - 5
cmd/web.go

@@ -257,7 +257,7 @@ func runWeb(c *cli.Context) error {
 		m.Get("/email2user", user.Email2User)
 		m.Get("/forget_password", user.ForgotPasswd)
 		m.Post("/forget_password", user.ForgotPasswdPost)
-		m.Get("/logout", user.SignOut)
+		m.Post("/logout", user.SignOut)
 	})
 	// ***** END: User *****
 
@@ -308,7 +308,7 @@ func runWeb(c *cli.Context) error {
 			m.Get("/followers", user.Followers)
 			m.Get("/following", user.Following)
 			m.Get("/stars", user.Stars)
-		})
+		}, context.InjectParamsUser())
 
 		m.Get("/attachments/:uuid", func(c *context.Context) {
 			attach, err := models.GetAttachmentByUUID(c.Params(":uuid"))
@@ -340,8 +340,8 @@ func runWeb(c *cli.Context) error {
 	}, ignSignIn)
 
 	m.Group("/:username", func() {
-		m.Get("/action/:action", user.Action)
-	}, reqSignIn)
+		m.Post("/action/:action", user.Action)
+	}, reqSignIn, context.InjectParamsUser())
 
 	if macaron.Env == macaron.DEV {
 		m.Get("/template/*", dev.TemplatePreview)
@@ -484,7 +484,7 @@ func runWeb(c *cli.Context) error {
 		})
 	}, reqSignIn, context.RepoAssignment(), reqRepoAdmin, context.RepoRef())
 
-	m.Get("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), repo.Action)
+	m.Post("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), repo.Action)
 	m.Group("/:username/:reponame", func() {
 		m.Get("/issues", repo.RetrieveLabels, repo.Issues)
 		m.Get("/issues/:index", repo.ViewIssue)

+ 1 - 1
gogs.go

@@ -16,7 +16,7 @@ import (
 	"github.com/gogs/gogs/pkg/setting"
 )
 
-const APP_VER = "0.11.76.1204"
+const APP_VER = "0.11.77.1206"
 
 func init() {
 	setting.AppVer = APP_VER

+ 30 - 0
pkg/context/user.go

@@ -0,0 +1,30 @@
+// Copyright 2018 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package context
+
+import (
+	"gopkg.in/macaron.v1"
+
+	"github.com/gogs/gogs/models"
+	"github.com/gogs/gogs/models/errors"
+)
+
+// ParamsUser is the wrapper type of the target user defined by URL parameter, namely ':username'.
+type ParamsUser struct {
+	*models.User
+}
+
+// InjectParamsUser returns a handler that retrieves target user based on URL parameter ':username',
+// and injects it as *ParamsUser.
+func InjectParamsUser() macaron.Handler {
+	return func(c *Context) {
+		user, err := models.GetUserByName(c.Params(":username"))
+		if err != nil {
+			c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err)
+			return
+		}
+		c.Map(&ParamsUser{user})
+	}
+}

+ 6 - 0
public/css/gogs.css

@@ -347,6 +347,9 @@ footer .ui.language .menu {
 .hide {
   display: none;
 }
+.display.inline {
+  display: inline;
+}
 .center {
   text-align: center;
 }
@@ -2934,6 +2937,9 @@ footer .ui.language .menu {
   margin-top: 5px;
   margin-right: 8px;
 }
+.user.profile .ui.card .profile-avatar {
+  height: 287px;
+}
 .user.profile .ui.card .header {
   word-break: break-all;
 }

+ 60 - 56
public/js/gogs.js

@@ -9,11 +9,11 @@ function initCommentPreviewTab($form) {
     $tabMenu.find('.item[data-tab="' + $tabMenu.data('preview') + '"]').click(function () {
         var $this = $(this);
         $.post($this.data('url'), {
-                "_csrf": csrf,
-                "mode": "gfm",
-                "context": $this.data('context'),
-                "text": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
-            },
+            "_csrf": csrf,
+            "mode": "gfm",
+            "context": $this.data('context'),
+            "text": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
+        },
             function (data) {
                 var $previewPanel = $form.find('.tab.segment[data-tab="' + $tabMenu.data('preview') + '"]');
                 $previewPanel.html(data);
@@ -39,11 +39,11 @@ function initEditPreviewTab($form) {
         $previewTab.click(function () {
             var $this = $(this);
             $.post($this.data('url'), {
-                    "_csrf": csrf,
-                    "mode": "gfm",
-                    "context": $this.data('context'),
-                    "text": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
-                },
+                "_csrf": csrf,
+                "mode": "gfm",
+                "context": $this.data('context'),
+                "text": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
+            },
                 function (data) {
                     var $previewPanel = $form.find('.tab.segment[data-tab="' + $tabMenu.data('preview') + '"]');
                     $previewPanel.html(data);
@@ -63,9 +63,9 @@ function initEditDiffTab($form) {
     $tabMenu.find('.item[data-tab="' + $tabMenu.data('diff') + '"]').click(function () {
         var $this = $(this);
         $.post($this.data('url'), {
-                "_csrf": csrf,
-                "content": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
-            },
+            "_csrf": csrf,
+            "content": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
+        },
             function (data) {
                 var $diffPreviewPanel = $form.find('.tab.segment[data-tab="' + $tabMenu.data('diff') + '"]');
                 $diffPreviewPanel.html(data);
@@ -221,7 +221,7 @@ function initRepository() {
                 window.location.href = $choice.data('url');
                 console.log($choice.data('url'))
             },
-            message: {noResults: $dropdown.data('no-results')}
+            message: { noResults: $dropdown.data('no-results') }
         });
     }
 
@@ -346,9 +346,9 @@ function initRepository() {
             }
 
             $.post($(this).data('update-url'), {
-                    "_csrf": csrf,
-                    "title": $editInput.val()
-                },
+                "_csrf": csrf,
+                "title": $editInput.val()
+            },
                 function (data) {
                     $editInput.val(data.title);
                     $issueTitle.text(data.title);
@@ -390,10 +390,10 @@ function initRepository() {
                     $editContentZone.hide();
 
                     $.post($editContentZone.data('update-url'), {
-                            "_csrf": csrf,
-                            "content": $textarea.val(),
-                            "context": $editContentZone.data('context')
-                        },
+                        "_csrf": csrf,
+                        "content": $textarea.val(),
+                        "context": $editContentZone.data('context')
+                    },
                         function (data) {
                             if (data.length == 0) {
                                 $renderContent.html($('#no-content').html());
@@ -500,14 +500,14 @@ function initRepository() {
         initFilterSearchDropdown('.choose.branch .dropdown');
     }
     if ($('.repository.view.pull').length > 0) {
-    	$('.comment.merge.box input[name=merge_style]').change(function () {
-    		if ($(this).val() === 'create_merge_commit') {
-				$('.commit.description.field').show();
-			} else {
-				$('.commit.description.field').hide();
-			}
-		})
-	}
+        $('.comment.merge.box input[name=merge_style]').change(function () {
+            if ($(this).val() === 'create_merge_commit') {
+                $('.commit.description.field').show();
+            } else {
+                $('.commit.description.field').hide();
+            }
+        })
+    }
 }
 
 function initWikiForm() {
@@ -521,11 +521,11 @@ function initWikiForm() {
                 setTimeout(function () {
                     // FIXME: still send render request when return back to edit mode
                     $.post($editArea.data('url'), {
-                            "_csrf": csrf,
-                            "mode": "gfm",
-                            "context": $editArea.data('context'),
-                            "text": plainText
-                        },
+                        "_csrf": csrf,
+                        "mode": "gfm",
+                        "context": $editArea.data('context'),
+                        "text": plainText
+                    },
                         function (data) {
                             preview.innerHTML = '<div class="markdown">' + data + '</div>';
                             emojify.run($('.editor-preview')[0]);
@@ -603,11 +603,11 @@ function setSimpleMDE($editArea) {
             setTimeout(function () {
                 // FIXME: still send render request when return back to edit mode
                 $.post($editArea.data('url'), {
-                        "_csrf": csrf,
-                        "mode": "gfm",
-                        "context": $editArea.data('context'),
-                        "text": plainText
-                    },
+                    "_csrf": csrf,
+                    "mode": "gfm",
+                    "context": $editArea.data('context'),
+                    "text": plainText
+                },
                     function (data) {
                         preview.innerHTML = '<div class="markdown">' + data + '</div>';
                         emojify.run($('.editor-preview')[0]);
@@ -652,10 +652,10 @@ function initEditor() {
     $('.js-quick-pull-choice-option').change(function () {
         if ($(this).val() == 'commit-to-new-branch') {
             $('.quick-pull-branch-name').show();
-            $('.quick-pull-branch-name input').prop('required',true);
+            $('.quick-pull-branch-name input').prop('required', true);
         } else {
             $('.quick-pull-branch-name').hide();
-            $('.quick-pull-branch-name input').prop('required',false);
+            $('.quick-pull-branch-name input').prop('required', false);
         }
     });
 
@@ -705,7 +705,7 @@ function initEditor() {
 
         var tree_path = parts.join('/');
         $('#tree_path').val(tree_path);
-        $('#preview-tab').data('context', $('#preview-tab').data('root-context') + tree_path.substring(0, tree_path.lastIndexOf("/")+1));
+        $('#preview-tab').data('context', $('#preview-tab').data('root-context') + tree_path.substring(0, tree_path.lastIndexOf("/") + 1));
     }).trigger('keyup');
 
     var $editArea = $('.repository.editor textarea#edit_area');
@@ -775,7 +775,7 @@ function initEditor() {
         value = value.split('/');
         value = value[value.length - 1];
 
-        $.getJSON($editFilename.data('ec-url-prefix')+value, function(editorconfig) {
+        $.getJSON($editFilename.data('ec-url-prefix') + value, function (editorconfig) {
             if (editorconfig.indent_style === 'tab') {
                 codeMirrorEditor.setOption("indentWithTabs", true);
                 codeMirrorEditor.setOption('extraKeys', {});
@@ -785,7 +785,7 @@ function initEditor() {
                 // - https://github.com/codemirror/CodeMirror/issues/988
                 // - https://codemirror.net/doc/manual.html#keymaps
                 codeMirrorEditor.setOption('extraKeys', {
-                    Tab: function(cm) {
+                    Tab: function (cm) {
                         var spaces = Array(parseInt(cm.getOption("indentUnit")) + 1).join(" ");
                         cm.replaceSelection(spaces);
                     }
@@ -1131,7 +1131,7 @@ function initWebhookSettings() {
         $($(this).data('target') + ' .nohighlight').each(function () {
             var $this = $(this);
             $this.removeClass('nohighlight');
-            setTimeout(function(){ hljs.highlightBlock($this[0]) }, 500);
+            setTimeout(function () { hljs.highlightBlock($this[0]) }, 500);
         })
     })
 
@@ -1213,7 +1213,7 @@ $(document).ready(function () {
         var filenameDict = {};
         $dropzone.dropzone({
             url: $dropzone.data('upload-url'),
-            headers: {"X-Csrf-Token": csrf},
+            headers: { "X-Csrf-Token": csrf },
             maxFiles: $dropzone.data('max-file'),
             maxFilesize: $dropzone.data('max-size'),
             acceptedFiles: ($dropzone.data('accepts') === '*/*') ? null : $dropzone.data('accepts'),
@@ -1336,6 +1336,10 @@ $(document).ready(function () {
             window.location.href = $this.data('done-url');
         });
     });
+    // To make arbitrary form element to behave like a submit button
+    $('.submit-button').click(function () {
+        $($(this).data('form')).submit();
+    });
 
     // Check or select on option to enable/disable target region
     $('.enable-system').change(function () {
@@ -1459,7 +1463,7 @@ $(function () {
     $('form').areYouSure();
 });
 
- // getByteLen counts bytes in a string's UTF-8 representation.
+// getByteLen counts bytes in a string's UTF-8 representation.
 function getByteLen(normalVal) {
     // Force string type
     normalVal = String(normalVal);
@@ -1467,19 +1471,19 @@ function getByteLen(normalVal) {
     var byteLen = 0;
     for (var i = 0; i < normalVal.length; i++) {
         var c = normalVal.charCodeAt(i);
-        byteLen += c < (1 <<  7) ? 1 :
-                   c < (1 << 11) ? 2 :
-                   c < (1 << 16) ? 3 :
-                   c < (1 << 21) ? 4 :
-                   c < (1 << 26) ? 5 :
-                   c < (1 << 31) ? 6 : Number.NaN;
+        byteLen += c < (1 << 7) ? 1 :
+            c < (1 << 11) ? 2 :
+                c < (1 << 16) ? 3 :
+                    c < (1 << 21) ? 4 :
+                        c < (1 << 26) ? 5 :
+                            c < (1 << 31) ? 6 : Number.NaN;
     }
     return byteLen;
 }
 
 function showMessageMaxLength(maxLen, textElemId, counterId) {
-    var $msg = $('#'+textElemId);
-    $('#'+counterId).html(maxLen - getByteLen($msg.val()));
+    var $msg = $('#' + textElemId);
+    $('#' + counterId).html(maxLen - getByteLen($msg.val()));
 
     var onMessageKey = function (e) {
         var $msg = $(this);
@@ -1492,7 +1496,7 @@ function showMessageMaxLength(maxLen, textElemId, counterId) {
             remainder = 0;
         }
 
-        $('#'+counterId).html(remainder);
+        $('#' + counterId).html(remainder);
     };
 
     $msg.keyup(onMessageKey).keydown(onMessageKey);

+ 5 - 0
public/less/_base.less

@@ -384,6 +384,11 @@ footer {
 .hide {
 	display: none;
 }
+.display {
+	&.inline {
+		display: inline;
+	}
+}
 .center {
 	text-align: center;
 }

+ 3 - 0
public/less/_user.less

@@ -60,6 +60,9 @@
 
 	&.profile {
 		.ui.card {
+			.profile-avatar {
+				height: 287px;
+			}
 			.header {
 				word-break: break-all;
 			}

+ 29 - 73
routes/user/profile.go

@@ -6,13 +6,11 @@ package user
 
 import (
 	"fmt"
-	"path"
 	"strings"
 
 	"github.com/Unknwon/paginater"
 
 	"github.com/gogs/gogs/models"
-	"github.com/gogs/gogs/models/errors"
 	"github.com/gogs/gogs/pkg/context"
 	"github.com/gogs/gogs/pkg/setting"
 	"github.com/gogs/gogs/pkg/tool"
@@ -24,59 +22,30 @@ const (
 	STARS     = "user/meta/stars"
 )
 
-func GetUserByName(c *context.Context, name string) *models.User {
-	user, err := models.GetUserByName(name)
-	if err != nil {
-		c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err)
-		return nil
-	}
-	return user
-}
-
-// GetUserByParams returns user whose name is presented in URL paramenter.
-func GetUserByParams(c *context.Context) *models.User {
-	return GetUserByName(c, c.Params(":username"))
-}
-
-func Profile(c *context.Context) {
-	uname := c.Params(":username")
-	// Special handle for FireFox requests favicon.ico.
-	if uname == "favicon.ico" {
-		c.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png"))
-		return
-	} else if strings.HasSuffix(uname, ".png") {
-		c.Error(404)
-		return
-	}
-
+func Profile(c *context.Context, puser *context.ParamsUser) {
 	isShowKeys := false
-	if strings.HasSuffix(uname, ".keys") {
+	if strings.HasSuffix(c.Params(":username"), ".keys") {
 		isShowKeys = true
 	}
 
-	ctxUser := GetUserByName(c, strings.TrimSuffix(uname, ".keys"))
-	if c.Written() {
-		return
-	}
-
 	// Show SSH keys.
 	if isShowKeys {
-		ShowSSHKeys(c, ctxUser.ID)
+		ShowSSHKeys(c, puser.ID)
 		return
 	}
 
-	if ctxUser.IsOrganization() {
+	if puser.IsOrganization() {
 		showOrgProfile(c)
 		return
 	}
 
-	c.Data["Title"] = ctxUser.DisplayName()
-	c.Data["PageIsUserProfile"] = true
-	c.Data["Owner"] = ctxUser
+	c.Title(puser.DisplayName())
+	c.PageIs("UserProfile")
+	c.Data["Owner"] = puser
 
-	orgs, err := models.GetOrgsByUserID(ctxUser.ID, c.IsLogged && (c.User.IsAdmin || c.User.ID == ctxUser.ID))
+	orgs, err := models.GetOrgsByUserID(puser.ID, c.IsLogged && (c.User.IsAdmin || c.User.ID == puser.ID))
 	if err != nil {
-		c.Handle(500, "GetOrgsByUserIDDesc", err)
+		c.ServerError("GetOrgsByUserIDDesc", err)
 		return
 	}
 
@@ -86,7 +55,7 @@ func Profile(c *context.Context) {
 	c.Data["TabName"] = tab
 	switch tab {
 	case "activity":
-		retrieveFeeds(c, ctxUser, -1, true)
+		retrieveFeeds(c, puser.User, -1, true)
 		if c.Written() {
 			return
 		}
@@ -96,65 +65,52 @@ func Profile(c *context.Context) {
 			page = 1
 		}
 
-		showPrivate := c.IsLogged && (ctxUser.ID == c.User.ID || c.User.IsAdmin)
+		showPrivate := c.IsLogged && (puser.ID == c.User.ID || c.User.IsAdmin)
 		c.Data["Repos"], err = models.GetUserRepositories(&models.UserRepoOptions{
-			UserID:   ctxUser.ID,
+			UserID:   puser.ID,
 			Private:  showPrivate,
 			Page:     page,
 			PageSize: setting.UI.User.RepoPagingNum,
 		})
 		if err != nil {
-			c.Handle(500, "GetRepositories", err)
+			c.ServerError("GetRepositories", err)
 			return
 		}
 
-		count := models.CountUserRepositories(ctxUser.ID, showPrivate)
+		count := models.CountUserRepositories(puser.ID, showPrivate)
 		c.Data["Page"] = paginater.New(int(count), setting.UI.User.RepoPagingNum, page, 5)
 	}
 
-	c.HTML(200, PROFILE)
+	c.Success(PROFILE)
 }
 
-func Followers(c *context.Context) {
-	u := GetUserByParams(c)
-	if c.Written() {
-		return
-	}
-	c.Data["Title"] = u.DisplayName()
+func Followers(c *context.Context, puser *context.ParamsUser) {
+	c.Title(puser.DisplayName())
+	c.PageIs("Followers")
 	c.Data["CardsTitle"] = c.Tr("user.followers")
-	c.Data["PageIsFollowers"] = true
-	c.Data["Owner"] = u
-	repo.RenderUserCards(c, u.NumFollowers, u.GetFollowers, FOLLOWERS)
+	c.Data["Owner"] = puser
+	repo.RenderUserCards(c, puser.NumFollowers, puser.GetFollowers, FOLLOWERS)
 }
 
-func Following(c *context.Context) {
-	u := GetUserByParams(c)
-	if c.Written() {
-		return
-	}
-	c.Data["Title"] = u.DisplayName()
+func Following(c *context.Context, puser *context.ParamsUser) {
+	c.Title(puser.DisplayName())
+	c.PageIs("Following")
 	c.Data["CardsTitle"] = c.Tr("user.following")
-	c.Data["PageIsFollowing"] = true
-	c.Data["Owner"] = u
-	repo.RenderUserCards(c, u.NumFollowing, u.GetFollowing, FOLLOWERS)
+	c.Data["Owner"] = puser
+	repo.RenderUserCards(c, puser.NumFollowing, puser.GetFollowing, FOLLOWERS)
 }
 
 func Stars(c *context.Context) {
 
 }
 
-func Action(c *context.Context) {
-	u := GetUserByParams(c)
-	if c.Written() {
-		return
-	}
-
+func Action(c *context.Context, puser *context.ParamsUser) {
 	var err error
 	switch c.Params(":action") {
 	case "follow":
-		err = models.FollowUser(c.User.ID, u.ID)
+		err = models.FollowUser(c.UserID(), puser.ID)
 	case "unfollow":
-		err = models.UnfollowUser(c.User.ID, u.ID)
+		err = models.UnfollowUser(c.UserID(), puser.ID)
 	}
 
 	if err != nil {
@@ -164,7 +120,7 @@ func Action(c *context.Context) {
 
 	redirectTo := c.Query("redirect_to")
 	if !tool.IsSameSiteURLPath(redirectTo) {
-		redirectTo = u.HomeLink()
+		redirectTo = puser.HomeLink()
 	}
 	c.Redirect(redirectTo)
 }

+ 1 - 1
templates/.VERSION

@@ -1 +1 @@
-0.11.76.1204
+0.11.77.1206

+ 7 - 3
templates/base/head.tmpl

@@ -160,9 +160,13 @@
 												{{end}}
 
 												<div class="divider"></div>
-												<a class="item" href="{{AppSubURL}}/user/logout">
-													<i class="octicon octicon-sign-out"></i> {{.i18n.Tr "sign_out"}}
-												</a>
+
+												<form id="logout-form" class="item" action="{{AppSubURL}}/user/logout" method="POST">
+													{{.CSRFTokenHTML}}
+													<div class="submit-button" data-form="#logout-form">
+														<i class="octicon octicon-sign-out"></i> {{.i18n.Tr "sign_out"}}
+													</div>
+												</form>
 											</div><!-- end content avatar menu -->
 										</div><!-- end dropdown avatar menu -->
 									</div><!-- end signed user right menu -->

+ 22 - 16
templates/repo/header.tmpl

@@ -20,22 +20,28 @@
 
 					{{if not $.IsGuest}}
 						<div class="ui right">
-							<div class="ui labeled button" tabindex="0">
-								<a class="ui basic button" href="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}un{{end}}watch?redirect_to={{$.Link}}">
-									<i class="eye{{if not $.IsWatchingRepo}} slash outline{{end}} icon"></i>{{if $.IsWatchingRepo}}{{$.i18n.Tr "repo.unwatch"}}{{else}}{{$.i18n.Tr "repo.watch"}}{{end}}
-								</a>
-								<a class="ui basic label" href="{{.Link}}/watchers">
-									{{.NumWatches}}
-								</a>
-							</div>
-							<div class="ui labeled button" tabindex="0">
-								<a class="ui basic button" href="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}un{{end}}star?redirect_to={{$.Link}}">
-									<i class="star{{if not $.IsStaringRepo}} outline{{end}} icon"></i>{{if $.IsStaringRepo}}{{$.i18n.Tr "repo.unstar"}}{{else}}{{$.i18n.Tr "repo.star"}}{{end}}
-								</a>
-								<a class="ui basic label" href="{{.Link}}/stars">
-									{{.NumStars}}
-								</a>
-							</div>
+							<form class="display inline" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}un{{end}}watch?redirect_to={{$.Link}}" method="POST">
+								{{$.CSRFTokenHTML}}
+								<div class="ui labeled button" tabindex="0">
+									<button class="ui basic button">
+										<i class="eye{{if not $.IsWatchingRepo}} slash outline{{end}} icon"></i>{{if $.IsWatchingRepo}}{{$.i18n.Tr "repo.unwatch"}}{{else}}{{$.i18n.Tr "repo.watch"}}{{end}}
+									</button>
+									<a class="ui basic label" href="{{.Link}}/watchers">
+										{{.NumWatches}}
+									</a>
+								</div>
+							</form>
+							<form class="display inline" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}un{{end}}star?redirect_to={{$.Link}}" method="POST">
+								{{$.CSRFTokenHTML}}
+								<div class="ui labeled button" tabindex="0">
+									<button class="ui basic button">
+										<i class="star{{if not $.IsStaringRepo}} outline{{end}} icon"></i>{{if $.IsStaringRepo}}{{$.i18n.Tr "repo.unstar"}}{{else}}{{$.i18n.Tr "repo.star"}}{{end}}
+									</button>
+									<a class="ui basic label" href="{{.Link}}/stars">
+										{{.NumStars}}
+									</a>
+								</div>
+							</form>
 							{{if .CanBeForked}}
 								<div class="ui labeled button" tabindex="0">
 									<a class="ui basic button {{if eq .OwnerID $.LoggedUserID}}poping up{{end}}" href="{{AppSubURL}}/repo/fork/{{.ID}}">

+ 21 - 11
templates/user/profile.tmpl

@@ -5,12 +5,12 @@
 			<div class="ui five wide column">
 				<div class="ui card">
 					{{if eq .LoggedUserName .Owner.Name}}
-						<a class="image poping up" href="{{AppSubURL}}/user/settings/avatar" id="profile-avatar" data-content="{{.i18n.Tr "user.change_avatar"}}" data-variation="inverted tiny" data-position="bottom center">
-							<img src="{{AppendAvatarSize .Owner.RelAvatarLink 290}}" title="{{.Owner.Name}}"/>
+						<a class="profile-avatar image poping up" href="{{AppSubURL}}/user/settings/avatar" id="profile-avatar" data-content="{{.i18n.Tr "user.change_avatar"}}" data-variation="inverted tiny" data-position="bottom center">
+							<img src="{{AppendAvatarSize .Owner.RelAvatarLink 287}}" title="{{.Owner.Name}}"/>
 						</a>
 					{{else}}
-						<span class="image">
-							<img src="{{AppendAvatarSize .Owner.RelAvatarLink 290}}" title="{{.Owner.Name}}"/>
+						<span class="profile-avatar image">
+							<img src="{{AppendAvatarSize .Owner.RelAvatarLink 287}}" title="{{.Owner.Name}}"/>
 						</span>
 					{{end}}
 					<div class="content">
@@ -61,13 +61,23 @@
 							</li>
 							{{end}}
 							{{if and .IsLogged (ne .LoggedUserName .Owner.Name)}}
-							<li class="follow">
-								{{if .LoggedUser.IsFollowing .Owner.ID}}
-								<a class="ui basic red button" href="{{.Link}}/action/unfollow?redirect_to={{$.Link}}"><i class="octicon octicon-person"></i> {{.i18n.Tr "user.unfollow"}}</a>
-								{{else}}
-								<a class="ui basic green button" href="{{.Link}}/action/follow?redirect_to={{$.Link}}"><i class="octicon octicon-person"></i> {{.i18n.Tr "user.follow"}}</a>
-								{{end}}
-							</li>
+								<li class="follow">
+									{{if .LoggedUser.IsFollowing .Owner.ID}}
+										<form action="{{.Link}}/action/unfollow?redirect_to={{$.Link}}" method="POST">
+											{{.CSRFTokenHTML}}
+											<button class="ui basic red button">
+												<i class="octicon octicon-person"></i> {{.i18n.Tr "user.unfollow"}}
+											</button>
+										</form>
+									{{else}}
+										<form action="{{.Link}}/action/follow?redirect_to={{$.Link}}" method="POST">
+											{{.CSRFTokenHTML}}
+											<button class="ui basic green button">
+												<i class="octicon octicon-person"></i> {{.i18n.Tr "user.follow"}}
+											</button>
+										</form>
+									{{end}}
+								</li>
 							{{end}}
 						</ul>
 					</div>