Skip to content

Commit aea3a1f

Browse files
committed
BLENDER: Add Admin page for Issues or Comments that contain external links
1 parent 6882177 commit aea3a1f

File tree

9 files changed

+283
-15
lines changed

9 files changed

+283
-15
lines changed

models/issues/comment.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,3 +1313,17 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error {
13131313
}
13141314
return committer.Commit()
13151315
}
1316+
1317+
// GetRecentComments returns the most recent issue comments
1318+
func GetRecentComments(ctx context.Context, opts *db.ListOptions) ([]*Comment, error) {
1319+
sess := db.GetEngine(ctx).
1320+
Where("type = ?", CommentTypeComment).
1321+
OrderBy("created_unix DESC")
1322+
1323+
if opts != nil {
1324+
sess = db.SetSessionPagination(sess, opts)
1325+
}
1326+
1327+
comments := make([]*Comment, 0, opts.PageSize)
1328+
return comments, sess.Find(&comments)
1329+
}

models/issues/issue.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,3 +824,17 @@ func ChangeIssueTimeEstimate(ctx context.Context, issue *Issue, doer *user_model
824824
return nil
825825
})
826826
}
827+
828+
// GetRecentIssues returns the most recently created issues
829+
func GetRecentIssues(ctx context.Context, opts *db.ListOptions) ([]*Issue, error) {
830+
sess := db.GetEngine(ctx).
831+
Where("is_pull = ?", false).
832+
OrderBy("created_unix DESC")
833+
834+
if opts != nil {
835+
sess = db.SetSessionPagination(sess, opts)
836+
}
837+
838+
issues := make([]*Issue, 0, opts.PageSize)
839+
return issues, sess.Find(&issues)
840+
}

options/locale/locale_en-US.ini

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2957,8 +2957,10 @@ first_page = First
29572957
last_page = Last
29582958
total = Total: %d
29592959
settings = Admin Settings
2960+
spam_management = Spam Management
29602961
spamreports = Spam Reports
2961-
users_with_links = Users (Potential Spam)
2962+
users_with_links = Users with Links
2963+
issues_with_links = Issues/Comments with Links
29622964
29632965
dashboard.new_version_hint = Gitea %s is now available, you are running %s. Check <a target="_blank" rel="noreferrer" href="%s">the blog</a> for more details.
29642966
dashboard.statistic = Summary

routers/utils/utils.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,17 @@ import (
1212
func SanitizeFlashErrorString(x string) string {
1313
return strings.ReplaceAll(html.EscapeString(x), "\n", "<br>")
1414
}
15+
16+
func ContainsHyperlink(text string) bool {
17+
text = strings.ToLower(text)
18+
return strings.Contains(text, "http://") || strings.Contains(text, "https://")
19+
}
20+
21+
func ContainsExcludedDomain(snippet string, domains []string) bool {
22+
for _, domain := range domains {
23+
if strings.Contains(snippet, domain) {
24+
return true
25+
}
26+
}
27+
return false
28+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package admin
2+
3+
import (
4+
"net/http"
5+
"sort"
6+
"strings"
7+
"time"
8+
9+
"code.gitea.io/gitea/modules/log"
10+
11+
issue_model "code.gitea.io/gitea/models/issues"
12+
repo_model "code.gitea.io/gitea/models/repo"
13+
"code.gitea.io/gitea/routers/utils"
14+
"code.gitea.io/gitea/modules/setting"
15+
"code.gitea.io/gitea/modules/templates"
16+
"code.gitea.io/gitea/models/db"
17+
"code.gitea.io/gitea/services/context"
18+
user_model "code.gitea.io/gitea/models/user"
19+
)
20+
21+
const tplIssuesWithLinks templates.TplName = "admin/issues_with_links"
22+
23+
// item with link in content
24+
type linkItem struct {
25+
Type string
26+
Content string
27+
User *user_model.User
28+
UserCreated time.Time
29+
RepoLink string
30+
ItemLink string
31+
Created time.Time
32+
}
33+
34+
// IssuesWithLinks shows issues and comments with external links
35+
func IssuesWithLinks(ctx *context.Context) {
36+
ctx.Data["Title"] = ctx.Tr("admin.issues_with_links")
37+
ctx.Data["PageIsIssuesWithLinks"] = true
38+
39+
page := ctx.FormInt("page")
40+
if page <= 1 {
41+
page = 1
42+
}
43+
44+
// fetch recent issues and comments
45+
limit := setting.UI.Admin.UserPagingNum
46+
issues, err := issue_model.GetRecentIssues(ctx, &db.ListOptions{Page: page, PageSize: limit})
47+
if err != nil {
48+
ctx.ServerError("GetRecentIssues", err)
49+
return
50+
}
51+
comments, err := issue_model.GetRecentComments(ctx, &db.ListOptions{Page: page, PageSize: limit})
52+
if err != nil {
53+
ctx.ServerError("GetRecentComments", err)
54+
return
55+
}
56+
57+
var excludedDomains = []string{
58+
"localhost:3000",
59+
"github.com",
60+
}
61+
62+
var items []linkItem
63+
appendIfHasLink := func(typeLabel, content, itemLink, repoLink string, created time.Time, u *user_model.User) {
64+
if !utils.ContainsHyperlink(content) {
65+
return
66+
}
67+
68+
snippet := extractFirstLinkSnippet(content)
69+
if snippet == "" || utils.ContainsExcludedDomain(snippet, excludedDomains) {
70+
return
71+
}
72+
73+
items = append(items, linkItem{
74+
Type: typeLabel,
75+
Content: snippet,
76+
User: u,
77+
UserCreated: u.CreatedUnix.AsTime(),
78+
RepoLink: repoLink,
79+
ItemLink: itemLink,
80+
Created: created,
81+
})
82+
}
83+
84+
for _, issue := range issues {
85+
86+
if issue.Content == "" {
87+
continue
88+
}
89+
90+
if issue.Repo == nil {
91+
issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
92+
if err != nil {
93+
log.Warn("Could not load repo for issue %d: %v", issue.ID, err)
94+
continue
95+
}
96+
}
97+
98+
u, err := user_model.GetUserByID(ctx, issue.PosterID)
99+
if err != nil {
100+
continue
101+
}
102+
103+
appendIfHasLink("Issue", issue.Content, issue.HTMLURL(), issue.Repo.HTMLURL(), issue.CreatedUnix.AsTime(), u)
104+
}
105+
106+
for _, comment := range comments {
107+
if comment.Issue == nil {
108+
comment.Issue, err = issue_model.GetIssueByID(ctx, comment.IssueID)
109+
if err != nil {
110+
log.Warn("Could not load issue for comment %d: %v", comment.ID, err)
111+
continue
112+
}
113+
}
114+
115+
if comment.Issue.Repo == nil {
116+
comment.Issue.Repo, err = repo_model.GetRepositoryByID(ctx, comment.Issue.RepoID)
117+
if err != nil {
118+
log.Warn("Could not load repo for issue %d: %v", comment.Issue.ID, err)
119+
continue
120+
}
121+
}
122+
123+
124+
if comment.Content == "" {
125+
continue
126+
}
127+
128+
u, err := user_model.GetUserByID(ctx, comment.PosterID)
129+
if err != nil {
130+
continue
131+
}
132+
133+
appendIfHasLink("Comment", comment.Content, comment.HTMLURL(ctx), comment.Issue.Repo.HTMLURL(), comment.CreatedUnix.AsTime(), u)
134+
}
135+
136+
// Sort by user creation date desc
137+
sort.Slice(items, func(i, j int) bool {
138+
return items[i].UserCreated.After(items[j].UserCreated)
139+
})
140+
141+
total := len(items)
142+
start := (page - 1) * limit
143+
end := start + limit
144+
if start > total {
145+
start = total
146+
}
147+
if end > total {
148+
end = total
149+
}
150+
paged := items[start:end]
151+
152+
ctx.Data["Items"] = paged
153+
ctx.Data["Total"] = total
154+
155+
// pager
156+
pager := context.NewPagination(total, limit, page, 5)
157+
pager.AddParamFromRequest(ctx.Req)
158+
ctx.Data["Page"] = pager
159+
160+
ctx.HTML(http.StatusOK, tplIssuesWithLinks)
161+
}
162+
163+
// for brevity, just return the first 200 chars containing http[s]
164+
func extractFirstLinkSnippet(text string) string {
165+
lower := strings.ToLower(text)
166+
i := strings.Index(lower, "http://")
167+
if i < 0 {
168+
i = strings.Index(lower, "https://")
169+
}
170+
if i < 0 {
171+
return ""
172+
}
173+
start := i
174+
end := start + 200
175+
if end > len(text) {
176+
end = len(text)
177+
}
178+
return text[start:end]
179+
}

routers/web/admin/users_with_links.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ package admin
22

33
import (
44
"net/http"
5-
"strings"
65

76
user_model "code.gitea.io/gitea/models/user"
7+
"code.gitea.io/gitea/routers/utils"
88
"code.gitea.io/gitea/modules/optional"
99
"code.gitea.io/gitea/modules/setting"
1010
"code.gitea.io/gitea/modules/templates"
@@ -17,7 +17,7 @@ const tplUsersWithLinks templates.TplName = "admin/users_with_links"
1717
// UsersWithLinks renders a list of users that contain hyperlinks in bio fields
1818
func UsersWithLinks(ctx *context.Context) {
1919
ctx.Data["Title"] = ctx.Tr("admin.users.with_links")
20-
ctx.Data["PageIsAdminUsers"] = true
20+
ctx.Data["PageIsUsersWithLinks"] = true
2121

2222
// Parse filters from query parameters
2323
statusActive := ctx.FormString("status_filter[is_active]")
@@ -60,8 +60,8 @@ func UsersWithLinks(ctx *context.Context) {
6060
// Filter users with hyperlinks in bio fields
6161
filtered := make([]*user_model.User, 0, len(users))
6262
for _, u := range users {
63-
if containsHyperlink(u.FullName) || containsHyperlink(u.Description) ||
64-
containsHyperlink(u.Location) || containsHyperlink(u.Website) {
63+
if utils.ContainsHyperlink(u.FullName) || utils.ContainsHyperlink(u.Description) ||
64+
utils.ContainsHyperlink(u.Location) || utils.ContainsHyperlink(u.Website) {
6565
filtered = append(filtered, u)
6666
}
6767
}
@@ -77,8 +77,3 @@ func UsersWithLinks(ctx *context.Context) {
7777

7878
ctx.HTML(http.StatusOK, tplUsersWithLinks)
7979
}
80-
81-
func containsHyperlink(text string) bool {
82-
text = strings.ToLower(text)
83-
return strings.Contains(text, "http://") || strings.Contains(text, "https://")
84-
}

routers/web/web.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -761,15 +761,19 @@ func registerWebRoutes(m *web.Router) {
761761
})
762762

763763
// BLENDER: spam reporting
764-
m.Group("/users_with_links", func() {
765-
m.Get("", admin.UsersWithLinks)
766-
})
767764
m.Group("/spamreports", func() {
768765
m.Get("", admin.SpamReports)
769766
m.Post("", admin.SpamReportsPost)
770767
})
771768
m.Post("/purge_spammer", admin.PurgeSpammerPost)
772769

770+
m.Group("/users_with_links", func() {
771+
m.Get("", admin.UsersWithLinks)
772+
})
773+
m.Group("/issues_with_links", func() {
774+
m.Get("", admin.IssuesWithLinks)
775+
})
776+
773777
m.Group("/orgs", func() {
774778
m.Get("", admin.Organizations)
775779
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin issues")}}
2+
<div class="admin-setting-content">
3+
<h4 class="ui top attached header">
4+
{{ctx.Locale.Tr "admin.issues_with_links"}} ({{.Total}})
5+
</h4>
6+
<div class="ui attached table segment">
7+
<table class="ui very basic striped table unstackable">
8+
<thead>
9+
<tr>
10+
<th>Type</th>
11+
<th>Found Link(s)</th>
12+
<th>Created</th>
13+
<th>{{ctx.Locale.Tr "admin.users.name"}}</th>
14+
<th>User Created</th>
15+
<th>Repository</th>
16+
</tr>
17+
</thead>
18+
<tbody>
19+
{{range .Items}}
20+
<tr>
21+
<td>{{.Type}}</td>
22+
<td class="gt-ellipsis tw-max-w-96"><a href="{{.ItemLink}}" target="_blank">{{.Content}}</a></td>
23+
<td>{{DateUtils.AbsoluteShort .Created}}</td>
24+
<td><a href="{{.User.HomeLink}}">{{.User.Name}}</a></td>
25+
<td>{{DateUtils.AbsoluteShort .UserCreated}}</td>
26+
<td><a href="{{.RepoLink}}" target="_blank">{{.RepoLink}}</a></td>
27+
</tr>
28+
{{else}}
29+
<tr class="no-results-row">
30+
<td class="tw-text-center" colspan="5">{{ctx.Locale.Tr "no_results_found"}}</td>
31+
</tr>
32+
{{end}}
33+
</tbody>
34+
</table>
35+
</div>
36+
{{template "base/paginate" .}}
37+
</div>
38+
{{template "admin/layout_footer" .}}

templates/admin/navbar.tmpl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
</a>
1414
</div>
1515
</details>
16-
<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications .PageIsSpamReports}}open{{end}}>
16+
<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
1717
<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
1818
<div class="menu">
1919
<a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths">
@@ -28,12 +28,20 @@
2828
<a class="{{if .PageIsAdminEmails}}active {{end}}item" href="{{AppSubUrl}}/-/admin/emails">
2929
{{ctx.Locale.Tr "admin.emails"}}
3030
</a>
31+
</div>
32+
</details>
33+
<details class="item toggleable-item" {{if or .PageIsSpamReports .PageIsUsersWithLinks .PageIsIssuesWithLinks}}open{{end}}>
34+
<summary>{{ctx.Locale.Tr "admin.spam_management"}}</summary>
35+
<div class="menu">
3136
<a class="{{if .PageIsSpamReports}}active {{end}}item" href="{{AppSubUrl}}/-/admin/spamreports">
3237
{{ctx.Locale.Tr "admin.spamreports"}}
3338
</a>
34-
<a class="{{if .PageIsSpamReports}}active {{end}}item" href="{{AppSubUrl}}/-/admin/users_with_links">
39+
<a class="{{if .PageIsUsersWithLinks}}active {{end}}item" href="{{AppSubUrl}}/-/admin/users_with_links">
3540
{{ctx.Locale.Tr "admin.users_with_links"}}
3641
</a>
42+
<a class="{{if .PageIsIssuesWithLinks}}active {{end}}item" href="{{AppSubUrl}}/-/admin/issues_with_links">
43+
{{ctx.Locale.Tr "admin.issues_with_links"}}
44+
</a>
3745
</div>
3846
</details>
3947
<details class="item toggleable-item" {{if or .PageIsAdminRepositories (and .EnablePackages .PageIsAdminPackages)}}open{{end}}>

0 commit comments

Comments
 (0)