Introduced length limits

This commit is contained in:
Eduard Urbach 2019-08-31 16:52:42 +09:00
parent 9338f7bdeb
commit 3ef41b1cdd
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
21 changed files with 110 additions and 80 deletions

6
arn/limits/limits.go Normal file
View File

@ -0,0 +1,6 @@
package limits
const (
DefaultTextMaxLength = 100
DefaultTextAreaMaxLength = 20000
)

View File

@ -25,7 +25,7 @@ func AMVs(originalTerm string, maxLength int) []*arn.AMV {
text := strings.ToLower(amv.Title.Canonical)
similarity := stringutils.AdvancedStringSimilarity(term, text)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
results = append(results, &Result{
obj: amv,
similarity: similarity,
@ -36,7 +36,7 @@ func AMVs(originalTerm string, maxLength int) []*arn.AMV {
text = strings.ToLower(amv.Title.Native)
similarity = stringutils.AdvancedStringSimilarity(term, text)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
results = append(results, &Result{
obj: amv,
similarity: similarity,

View File

@ -1,16 +1,24 @@
package search
import (
"fmt"
"github.com/aerogo/flow"
"github.com/animenotifier/notify.moe/arn"
)
// MinimumStringSimilarity is the minimum JaroWinkler distance we accept for search results.
const MinimumStringSimilarity = 0.89
// MinStringSimilarity is the minimum JaroWinkler distance we accept for search results.
const MinStringSimilarity = 0.89
// MaxSearchTermLength defines how long the search term can be.
const MaxSearchTermLength = 100
// popularityDamping reduces the factor of popularity in search results.
const popularityDamping = 0.0009
// ErrTermTooLong is used when the search term is too long
var ErrTermTooLong = fmt.Errorf("Search term is too long (maximum %d characters)", MaxSearchTermLength)
// Result ...
type Result struct {
obj interface{}
@ -23,6 +31,10 @@ func All(term string, maxUsers, maxAnime, maxPosts, maxThreads, maxTracks, maxCh
return nil, nil, nil, nil, nil, nil, nil, nil
}
if len(term) > MaxSearchTermLength {
return nil, nil, nil, nil, nil, nil, nil, nil
}
var (
userResults []*arn.User
animeResults []*arn.Anime

View File

@ -42,7 +42,7 @@ func Anime(originalTerm string, maxLength int) []*arn.Anime {
// Canonical title
similarity := check(anime.Title.Canonical)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
add(anime, similarity)
continue
}
@ -50,7 +50,7 @@ func Anime(originalTerm string, maxLength int) []*arn.Anime {
// English
similarity = check(anime.Title.English)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
add(anime, similarity)
continue
}
@ -58,7 +58,7 @@ func Anime(originalTerm string, maxLength int) []*arn.Anime {
// Romaji
similarity = check(anime.Title.Romaji)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
add(anime, similarity)
continue
}
@ -67,7 +67,7 @@ func Anime(originalTerm string, maxLength int) []*arn.Anime {
for _, synonym := range anime.Title.Synonyms {
similarity := check(synonym)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
add(anime, similarity)
goto nextAnime
}
@ -76,7 +76,7 @@ func Anime(originalTerm string, maxLength int) []*arn.Anime {
// Japanese
similarity = check(anime.Title.Japanese)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
add(anime, similarity)
continue
}

View File

@ -25,7 +25,7 @@ func Companies(originalTerm string, maxLength int) []*arn.Company {
text := strings.ToLower(stringutils.RemoveSpecialCharacters(company.Name.English))
similarity := stringutils.AdvancedStringSimilarity(term, text)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
results = append(results, &Result{
obj: company,
similarity: similarity,

View File

@ -25,7 +25,7 @@ func SoundTracks(originalTerm string, maxLength int) []*arn.SoundTrack {
text := strings.ToLower(track.Title.Canonical)
similarity := stringutils.AdvancedStringSimilarity(term, text)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
results = append(results, &Result{
obj: track,
similarity: similarity,
@ -36,7 +36,7 @@ func SoundTracks(originalTerm string, maxLength int) []*arn.SoundTrack {
text = strings.ToLower(track.Title.Native)
similarity = stringutils.AdvancedStringSimilarity(term, text)
if similarity >= MinimumStringSimilarity {
if similarity >= MinStringSimilarity {
results = append(results, &Result{
obj: track,
similarity: similarity,

View File

@ -23,7 +23,7 @@ func Users(originalTerm string, maxLength int) []*arn.User {
// Similarity check
similarity := stringutils.AdvancedStringSimilarity(term, text)
if similarity < MinimumStringSimilarity {
if similarity < MinStringSimilarity {
continue
}

View File

@ -1,2 +1,2 @@
component FuzzySearch(value string)
input#search.action(data-action="search", data-trigger="input", type="search", autocomplete="off", autocorrect="off", autocapitalize="none", spellcheck="false", placeholder="Search...", title="Shortcut: F", value=value)
input#search.action(data-action="search", data-trigger="input", type="search", autocomplete="off", autocorrect="off", autocapitalize="none", spellcheck="false", placeholder="Search...", title="Shortcut: F", maxlength="100", value=value)

View File

@ -1,7 +1,7 @@
component InputText(id string, value string, label string, placeholder string)
component InputText(id string, value string, label string, placeholder string, maxLength int)
.widget-section
label(for=id)= label + ":"
input.widget-ui-element.action(id=id, data-field=id, type="text", value=value, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")
input.widget-ui-element.action(id=id, data-field=id, type="text", value=value, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change", maxlength=maxLength)
component InputBool(id string, value bool, label string, title string)
.widget-section
@ -16,10 +16,10 @@ component InputBool(id string, value bool, label string, title string)
span OFF
//- input.widget-ui-element.action(id=id, data-field=id, type="checkbox", value=value, checked=value, data-action="save", data-trigger="change")
component InputTextArea(id string, value string, label string, placeholder string)
component InputTextArea(id string, value string, label string, placeholder string, maxLength int)
.widget-section
label(for=id)= label + ":"
textarea.widget-ui-element.action(id=id, data-field=id, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")= value
textarea.widget-ui-element.action(id=id, data-field=id, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change", maxlength=maxLength)= value
component InputNumber(id string, value float64, label string, placeholder string, min string, max string, step string)
.widget-section

View File

@ -4,7 +4,7 @@ component NewPostArea(parent arn.PostParent, user *arn.User, placeholder string)
.post-author
Avatar(user)
textarea#new-post-text.post-content(placeholder=placeholder + "...", aria-label=placeholder)
textarea#new-post-text.post-content(placeholder=placeholder + "...", aria-label=placeholder, maxlength=limits.DefaultTextAreaMaxLength)
if !arn.IsLocked(parent)
NewPostActions(parent, false)

View File

@ -40,7 +40,7 @@ component Postable(post arn.Postable, user *arn.User, includeReplies bool, showP
if post.TypeName() == "Thread"
input.post-title-input.hidden(id="title-" + post.GetID(), value=post.TitleByUser(user), type="text", placeholder="Thread title")
textarea.post-text-input.hidden(id="source-" + post.GetID())= post.GetText()
textarea.post-text-input.hidden(id="source-" + post.GetID(), maxlength=limits.DefaultTextAreaMaxLength)= post.GetText()
.buttons.hidden(id="edit-toolbar-" + post.GetID())
a.button.post-save.action(data-action="savePost", data-trigger="click", data-id=post.GetID())

View File

@ -26,7 +26,7 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn.
InputBool("Private", item.Private, "Private", "Hidden entry")
.mountable
InputTextArea("Notes", item.Notes, "Notes", "Your notes")
InputTextArea("Notes", item.Notes, "Notes", "Your notes", 2000)
.buttons.mountable
a.button.mountable(href="/+" + viewUser.Nick + "/animelist/" + item.Status)

View File

@ -5,7 +5,7 @@ component NewThread(user *arn.User)
.widget
input#title.widget-ui-element(type="text", placeholder="Title")
textarea#text.widget-ui-element(placeholder="Content")
textarea#text.widget-ui-element(placeholder="Content", maxlength=limits.DefaultTextAreaMaxLength)
select#tag.widget-ui-element(value="general")
option(value="general") General

View File

@ -1,3 +1,3 @@
component MultiSearch
textarea.action(data-action="multiSearchAnime", data-trigger="change", placeholder="Paste multiple anime titles here, one per line")
textarea.action(data-action="multiSearchAnime", data-trigger="change", placeholder="Paste multiple anime titles here, one per line", maxlength=limits.DefaultTextAreaMaxLength)
#multi-search-anime

View File

@ -1,7 +1,7 @@
package search
import (
"strings"
"net/http"
"github.com/animenotifier/notify.moe/utils"
@ -24,9 +24,12 @@ const (
// Get search page.
func Get(ctx aero.Context) error {
term := ctx.Get("term")
term = strings.TrimPrefix(term, "/")
user := utils.GetUser(ctx)
if len(term) > search.MaxSearchTermLength {
ctx.SetStatus(http.StatusRequestEntityTooLarge)
}
users, animes, posts, threads, tracks, characters, amvs, companies := search.All(
term,
maxUsers,
@ -50,9 +53,12 @@ func GetEmptySearch(ctx aero.Context) error {
// Anime search.
func Anime(ctx aero.Context) error {
term := ctx.Get("term")
term = strings.TrimPrefix(term, "/")
user := utils.GetUser(ctx)
if len(term) > search.MaxSearchTermLength {
ctx.SetStatus(http.StatusRequestEntityTooLarge)
}
animes := search.Anime(term, maxAnime)
return ctx.HTML(components.AnimeSearchResults(animes, user))
}
@ -60,9 +66,12 @@ func Anime(ctx aero.Context) error {
// Characters search.
func Characters(ctx aero.Context) error {
term := ctx.Get("term")
term = strings.TrimPrefix(term, "/")
user := utils.GetUser(ctx)
if len(term) > search.MaxSearchTermLength {
ctx.SetStatus(http.StatusRequestEntityTooLarge)
}
characters := search.Characters(term, maxCharacters)
return ctx.HTML(components.CharacterSearchResults(characters, user))
}
@ -70,29 +79,38 @@ func Characters(ctx aero.Context) error {
// Posts search.
func Posts(ctx aero.Context) error {
term := ctx.Get("term")
term = strings.TrimPrefix(term, "/")
user := utils.GetUser(ctx)
posts := search.Posts(term, maxPosts)
if len(term) > search.MaxSearchTermLength {
ctx.SetStatus(http.StatusRequestEntityTooLarge)
}
posts := search.Posts(term, maxPosts)
return ctx.HTML(components.PostsSearchResults(posts, user))
}
// Threads search.
func Threads(ctx aero.Context) error {
term := ctx.Get("term")
term = strings.TrimPrefix(term, "/")
user := utils.GetUser(ctx)
threads := search.Threads(term, maxThreads)
if len(term) > search.MaxSearchTermLength {
ctx.SetStatus(http.StatusRequestEntityTooLarge)
}
threads := search.Threads(term, maxThreads)
return ctx.HTML(components.ThreadsSearchResults(threads, user))
}
// SoundTracks search.
func SoundTracks(ctx aero.Context) error {
term := ctx.Get("term")
term = strings.TrimPrefix(term, "/")
user := utils.GetUser(ctx)
if len(term) > search.MaxSearchTermLength {
ctx.SetStatus(http.StatusRequestEntityTooLarge)
}
tracks := search.SoundTracks(term, maxSoundTracks)
return ctx.HTML(components.SoundTrackSearchResults(tracks, user))
}
@ -100,9 +118,12 @@ func SoundTracks(ctx aero.Context) error {
// AMVs search.
func AMVs(ctx aero.Context) error {
term := ctx.Get("term")
term = strings.TrimPrefix(term, "/")
user := utils.GetUser(ctx)
if len(term) > search.MaxSearchTermLength {
ctx.SetStatus(http.StatusRequestEntityTooLarge)
}
amvs := search.AMVs(term, maxAMVs)
return ctx.HTML(components.AMVSearchResults(amvs, user))
}
@ -110,7 +131,10 @@ func AMVs(ctx aero.Context) error {
// Users search.
func Users(ctx aero.Context) error {
term := ctx.Get("term")
term = strings.TrimPrefix(term, "/")
if len(term) > search.MaxSearchTermLength {
ctx.SetStatus(http.StatusRequestEntityTooLarge)
}
users := search.Users(term, maxUsers)
return ctx.HTML(components.UserSearchResults(users))
@ -119,7 +143,10 @@ func Users(ctx aero.Context) error {
// Companies search.
func Companies(ctx aero.Context) error {
term := ctx.Get("term")
term = strings.TrimPrefix(term, "/")
if len(term) > search.MaxSearchTermLength {
ctx.SetStatus(http.StatusRequestEntityTooLarge)
}
companies := search.Companies(term, maxCompanies)
return ctx.HTML(components.CompanySearchResults(companies))

View File

@ -9,20 +9,20 @@ component SettingsAccounts(user *arn.User)
Icon("cubes")
span Accounts
InputText("Accounts.AniList.Nick", user.Accounts.AniList.Nick, "AniList", "Your username on anilist.co")
InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io")
InputText("Accounts.MyAnimeList.Nick", user.Accounts.MyAnimeList.Nick, "MyAnimeList", "Your username on myanimelist.net")
InputText("Accounts.Discord.Nick", user.Accounts.Discord.Nick, "Discord", "Your username on Discord")
InputText("Accounts.AniList.Nick", user.Accounts.AniList.Nick, "AniList", "Your username on anilist.co", 30)
InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io", 30)
InputText("Accounts.MyAnimeList.Nick", user.Accounts.MyAnimeList.Nick, "MyAnimeList", "Your username on myanimelist.net", 30)
InputText("Accounts.Discord.Nick", user.Accounts.Discord.Nick, "Discord", "Your username on Discord", 30)
.widget.mountable(data-api="/api/user/" + user.ID)
h3.widget-title
Icon("gamepad")
span Games
InputText("Accounts.FinalFantasyXIV.Nick", user.Accounts.FinalFantasyXIV.Nick, "Final Fantasy XIV", "Your character name on FFXIV")
InputText("Accounts.FinalFantasyXIV.Nick", user.Accounts.FinalFantasyXIV.Nick, "Final Fantasy XIV", "Your character name on FFXIV", 30)
InputSelection("Accounts.FinalFantasyXIV.Server", user.Accounts.FinalFantasyXIV.Server, "Final Fantasy XIV - World", "Your server/world on FFXIV", arn.DataLists["ffxiv-servers"])
InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh")
InputText("Accounts.Overwatch.BattleTag", user.Accounts.Overwatch.BattleTag, "Overwatch", "Your battletag on Overwatch")
InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh", 30)
InputText("Accounts.Overwatch.BattleTag", user.Accounts.Overwatch.BattleTag, "Overwatch", "Your battletag on Overwatch", 30)
.widget.mountable
h3.widget-title

View File

@ -10,8 +10,8 @@ component SettingsInfo(user *arn.User)
span Info
InputSelection("Gender", user.Gender, "Gender", "Your gender", arn.DataLists["genders"])
InputText("BirthDay", user.BirthDay, "Birthday", "YYYY-MM-DD")
InputText("Website", user.Website, "Website", "Your homepage")
InputText("BirthDay", user.BirthDay, "Birthday", "YYYY-MM-DD", len("YYYY-MM-DD"))
InputText("Website", user.Website, "Website", "Your homepage", 100)
.widget.mountable(data-api="/api/settings/" + user.ID)
h3.widget-title

View File

@ -9,39 +9,14 @@ component SettingsPersonal(user *arn.User)
Icon("user")
span Personal
InputText("Nick", user.Nick, "Username", "Your username on notify.moe")
InputTextArea("Introduction", user.Introduction, "Introduction", "Tell us a little bit about yourself")
InputText("Nick", user.Nick, "Username", "Your username on notify.moe", 25)
InputTextArea("Introduction", user.Introduction, "Introduction", "Tell us a little bit about yourself", 2000)
.widget.mountable(data-api="/api/settings/" + user.ID)
h3.widget-title
Icon("camera")
span Avatar
//- .widget-section
//- label(for="Avatar.Source") Source:
//- select.widget-ui-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Settings().Avatar.Source, data-action="save", data-trigger="change")
//- option(value="") Automatic
//- option(value="Gravatar") Gravatar
//- option(value="URL") Link
//- option(value="FileSystem") Upload
//- //- URL input
//- if user.Settings().Avatar.Source == "URL"
//- InputText("Avatar.SourceURL", user.Settings().Avatar.SourceURL, "Link", "Post the link to the image here")
//- //- Gravatar preview image
//- if user.Settings().Avatar.Source == "Gravatar" || (user.Settings().Avatar.Source == "" && user.Avatar.Source == "Gravatar")
//- .profile-image-container.avatar-preview
//- img.profile-image.mountable(src=user.Gravatar(), alt="Gravatar (" + user.Email + ")", title="Gravatar (" + user.Email + ")")
//- //- URL preview image
//- if user.Settings().Avatar.Source == "URL" && user.Settings().Avatar.SourceURL != ""
//- .profile-image-container.avatar-preview
//- img.profile-image.mountable(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview")
//- //- File upload
//- if user.Settings().Avatar.Source == "FileSystem"
AvatarInput(user)
.widget.mountable(data-api="/api/settings/" + user.ID)

View File

@ -10,7 +10,7 @@ component Welcome(user *arn.User)
.mountable(data-api="/api/user/" + user.ID)
//- [^\\W\\s\\d]{1,24}[A-Za-z]{1}
InputText("Nick", user.CleanNick(), "Nick", "Your username on notify.moe")
InputText("Nick", user.CleanNick(), "Nick", "Your username on notify.moe", 25)
.footer.mountable
p Only letters and underscore.
@ -26,7 +26,7 @@ component Welcome(user *arn.User)
p.welcome-text.mountable Write a little introduction for your profile.
.mountable(data-api="/api/user/" + user.ID)
InputTextArea("Introduction", user.Introduction, "Introduction", "Tell us a little bit about yourself")
InputTextArea("Introduction", user.Introduction, "Introduction", "Tell us a little bit about yourself", 2000)
.footer.mountable
p Markdown allowed.

View File

@ -34,9 +34,6 @@ const fetchOptions: RequestInit = {
credentials: "same-origin"
}
// Error message
const searchErrorMessage = "Looks like the website is offline, please retry later."
// Speech recognition
let recognition: SpeechRecognition
@ -122,7 +119,7 @@ export async function search(arn: AnimeNotifier, search: HTMLInputElement, evt?:
// Start searching anime
fetch("/_/anime-search/" + term, fetchOptions)
.then(showResponseInElement(arn, url, "anime", results["anime"]))
.catch(_ => arn.statusMessage.showError(searchErrorMessage))
.catch(err => arn.statusMessage.showError(err))
requestIdleCallback(() => {
// Check that the term hasn't changed in the meantime
@ -138,7 +135,7 @@ export async function search(arn: AnimeNotifier, search: HTMLInputElement, evt?:
fetch(`/_/${key}-search/` + term, fetchOptions)
.then(showResponseInElement(arn, url, key, results[key]))
.catch(_ => arn.statusMessage.showError(searchErrorMessage))
.catch(err => arn.statusMessage.showError(err))
}
})
} catch(err) {

View File

@ -9,6 +9,7 @@ import (
"github.com/aerogo/api"
"github.com/animenotifier/notify.moe/arn"
"github.com/animenotifier/notify.moe/arn/limits"
"github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/utils"
)
@ -214,7 +215,13 @@ func renderStringField(b io.StringWriter, v *reflect.Value, field reflect.Struct
b.WriteString(components.InputSelection(idPrefix+field.Name, fieldValue.String(), field.Name, field.Tag.Get("tooltip"), values))
case field.Tag.Get("type") == "textarea":
b.WriteString(components.InputTextArea(idPrefix+field.Name, fieldValue.String(), field.Name, field.Tag.Get("tooltip")))
maxLength, err := strconv.Atoi(field.Tag.Get("maxLength"))
if err != nil {
maxLength = limits.DefaultTextAreaMaxLength
}
b.WriteString(components.InputTextArea(idPrefix+field.Name, fieldValue.String(), field.Name, field.Tag.Get("tooltip"), maxLength))
case field.Tag.Get("type") == "upload":
endpoint := field.Tag.Get("endpoint")
@ -224,7 +231,13 @@ func renderStringField(b io.StringWriter, v *reflect.Value, field reflect.Struct
b.WriteString(components.InputFileUpload(idPrefix+field.Name, field.Name, field.Tag.Get("filetype"), endpoint))
default:
b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, field.Tag.Get("tooltip")))
maxLength, err := strconv.Atoi(field.Tag.Get("maxLength"))
if err != nil {
maxLength = limits.DefaultTextMaxLength
}
b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, field.Tag.Get("tooltip"), maxLength))
}
if showPreview {